Replace Rusqlite by Sqlx and Actix by Axum (A lot of changes)
authorGreg Burri <greg.burri@gmail.com>
Sun, 3 Nov 2024 09:13:31 +0000 (10:13 +0100)
committerGreg Burri <greg.burri@gmail.com>
Sun, 3 Nov 2024 09:13:31 +0000 (10:13 +0100)
29 files changed:
.cargo/config.toml
Cargo.lock
Cargo.toml
README.md
TODO.md
backend/Cargo.toml
backend/src/config.rs
backend/src/consts.rs
backend/src/data/asynchronous.rs [deleted file]
backend/src/data/db.rs
backend/src/data/mod.rs
backend/src/data/utils.rs [new file with mode: 0644]
backend/src/email.rs
backend/src/main.rs
backend/src/model.rs
backend/src/services.rs
backend/src/services/webapi.rs
backend/src/utils.rs
backend/templates/base_with_header.html
backend/templates/message.html
backend/templates/message_base.html [deleted file]
backend/templates/message_without_user.html [new file with mode: 0644]
backend/templates/title.html
check_cargo_dependencies_upgrade.nu [new file with mode: 0644]
common/src/lib.rs
common/src/ron_api.rs
common/src/utils.rs
deploy.nu
frontend/Cargo.toml

index 73227e6..c0b281c 100644 (file)
@@ -1,2 +1,2 @@
 [target.aarch64-unknown-linux-gnu]
-linker = "aarch64-linux-gnu-gcc.exe"
\ No newline at end of file
+linker = "aarch64-linux-gnu-gcc.exe"
index 38f2ad2..84749ed 100644 (file)
 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-files"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689"
-dependencies = [
- "actix-http",
- "actix-service",
- "actix-utils",
- "actix-web",
- "askama_escape",
- "bitflags",
- "bytes",
- "derive_more",
- "futures-core",
- "http-range",
- "log",
- "mime",
- "mime_guess",
- "percent-encoding",
- "pin-project-lite",
-]
-
-[[package]]
-name = "actix-http"
-version = "3.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74"
-dependencies = [
- "actix-codec",
- "actix-rt",
- "actix-service",
- "actix-utils",
- "ahash 0.8.3",
- "base64 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"
+name = "addr2line"
+version = "0.24.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
 dependencies = [
- "quote",
- "syn 1.0.109",
+ "gimli",
 ]
 
 [[package]]
-name = "actix-router"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799"
-dependencies = [
- "bytestring",
- "http",
- "regex",
- "serde",
- "tracing",
-]
-
-[[package]]
-name = "actix-rt"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e"
-dependencies = [
- "futures-core",
- "tokio",
-]
-
-[[package]]
-name = "actix-server"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327"
-dependencies = [
- "actix-rt",
- "actix-service",
- "actix-utils",
- "futures-core",
- "futures-util",
- "mio",
- "num_cpus",
- "socket2",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "actix-service"
-version = "2.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a"
-dependencies = [
- "futures-core",
- "paste",
- "pin-project-lite",
-]
-
-[[package]]
-name = "actix-utils"
-version = "3.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
-dependencies = [
- "local-waker",
- "pin-project-lite",
-]
-
-[[package]]
-name = "actix-web"
-version = "4.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96"
-dependencies = [
- "actix-codec",
- "actix-http",
- "actix-macros",
- "actix-router",
- "actix-rt",
- "actix-server",
- "actix-service",
- "actix-utils",
- "actix-web-codegen",
- "ahash 0.7.6",
- "bytes",
- "bytestring",
- "cfg-if 1.0.0",
- "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"
+name = "adler2"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
-dependencies = [
- "getrandom",
- "once_cell",
- "version_check",
-]
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
 
 [[package]]
 name = "ahash"
-version = "0.8.3"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if 1.0.0",
- "getrandom",
  "once_cell",
  "version_check",
+ "zerocopy",
 ]
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.20"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
 dependencies = [
  "memchr",
 ]
 
 [[package]]
-name = "alloc-no-stdlib"
-version = "2.0.4"
+name = "allocator-api2"
+version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
 [[package]]
-name = "alloc-stdlib"
-version = "0.2.2"
+name = "android-tzdata"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
-dependencies = [
- "alloc-no-stdlib",
-]
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
 
 [[package]]
 name = "android_system_properties"
@@ -270,60 +61,70 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.2.6"
+version = "0.6.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f"
+checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
 dependencies = [
  "anstyle",
  "anstyle-parse",
+ "anstyle-query",
  "anstyle-wincon",
- "concolor-override",
- "concolor-query",
- "is-terminal",
+ "colorchoice",
+ "is_terminal_polyfill",
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle"
-version = "0.3.5"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.1.1"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
 dependencies = [
  "utf8parse",
 ]
 
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "anstyle-wincon"
-version = "0.2.0"
+version = "3.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
 dependencies = [
  "anstyle",
- "windows-sys 0.45.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "argon2"
-version = "0.5.0"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
 dependencies = [
  "base64ct",
  "blake2",
+ "cpufeatures",
  "password-hash",
 ]
 
 [[package]]
 name = "askama"
-version = "0.12.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47cbc3cf73fa8d9833727bbee4835ba5c421a0d65b72daf9a7b5d0e0f9cfb57e"
+checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
 dependencies = [
  "askama_derive",
  "askama_escape",
@@ -334,29 +135,30 @@ dependencies = [
 ]
 
 [[package]]
-name = "askama_actix"
-version = "0.14.0"
+name = "askama_axum"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4b0dd17cfe203b00ba3853a89fba459ecf24c759b738b244133330607c78e55"
+checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163"
 dependencies = [
- "actix-web",
  "askama",
+ "axum-core",
+ "http",
 ]
 
 [[package]]
 name = "askama_derive"
-version = "0.12.1"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c22fbe0413545c098358e56966ff22cdd039e10215ae213cfbd65032b119fc94"
+checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
 dependencies = [
+ "askama_parser",
  "basic-toml",
  "mime",
  "mime_guess",
- "nom",
  "proc-macro2",
  "quote",
  "serde",
- "syn 2.0.13",
+ "syn",
 ]
 
 [[package]]
@@ -365,23 +167,157 @@ version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
 
+[[package]]
+name = "askama_parser"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "autocfg"
-version = "1.1.0"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "axum"
+version = "0.7.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "axum-macros",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.1",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper 1.0.1",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-extra"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "cookie",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "serde",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+dependencies = [
+ "addr2line",
+ "cfg-if 1.0.0",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
 
 [[package]]
 name = "base64"
-version = "0.13.1"
+version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
 [[package]]
 name = "base64"
-version = "0.21.0"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
 [[package]]
 name = "base64ct"
@@ -391,18 +327,21 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "basic-toml"
-version = "0.1.2"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c0de75129aa8d0cceaf750b89013f0e08804d6ec61416da787b35ad0d7cddf1"
+checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "bitflags"
-version = "1.3.2"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "blake2"
@@ -422,55 +361,31 @@ dependencies = [
  "generic-array",
 ]
 
-[[package]]
-name = "brotli"
-version = "3.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
- "brotli-decompressor",
-]
-
-[[package]]
-name = "brotli-decompressor"
-version = "2.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
-]
-
 [[package]]
 name = "bumpalo"
-version = "3.12.0"
+version = "3.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
 
 [[package]]
-name = "bytes"
-version = "1.4.0"
+name = "byteorder"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
-name = "bytestring"
-version = "1.3.0"
+name = "bytes"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae"
-dependencies = [
- "bytes",
-]
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
 
 [[package]]
 name = "cc"
-version = "1.0.79"
+version = "1.1.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9"
 dependencies = [
- "jobserver",
+ "shlex",
 ]
 
 [[package]]
@@ -487,70 +402,73 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.24"
+version = "0.4.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
 dependencies = [
+ "android-tzdata",
  "iana-time-zone",
  "js-sys",
- "num-integer",
  "num-traits",
- "time 0.1.45",
  "wasm-bindgen",
- "winapi",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "chumsky"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
+dependencies = [
+ "hashbrown 0.14.5",
+ "stacker",
 ]
 
 [[package]]
 name = "clap"
-version = "4.2.1"
+version = "4.5.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3"
+checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
 dependencies = [
  "clap_builder",
  "clap_derive",
- "once_cell",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.2.1"
+version = "4.5.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f"
+checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
 dependencies = [
  "anstream",
  "anstyle",
- "bitflags",
  "clap_lex",
  "strsim",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "4.2.0"
+version = "4.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4"
+checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
 dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn",
 ]
 
 [[package]]
 name = "clap_lex"
-version = "0.4.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
 
 [[package]]
-name = "codespan-reporting"
-version = "0.11.1"
+name = "colorchoice"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
-dependencies = [
- "termcolor",
- "unicode-width",
-]
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
 name = "common"
@@ -564,15 +482,13 @@ dependencies = [
 
 [[package]]
 name = "comrak"
-version = "0.16.0"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784836d0812dade01579cc0cc9b1684847044e716fd7aa6bffbc172e42199500"
+checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
 dependencies = [
  "entities",
  "memchr",
  "once_cell",
- "pest",
- "pest_derive",
  "regex",
  "slug",
  "typed-arena",
@@ -580,18 +496,12 @@ dependencies = [
 ]
 
 [[package]]
-name = "concolor-override"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f"
-
-[[package]]
-name = "concolor-query"
-version = "0.3.3"
+name = "concurrent-queue"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
 dependencies = [
- "windows-sys 0.45.0",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -604,47 +514,77 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
 [[package]]
 name = "convert_case"
-version = "0.4.0"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
 
 [[package]]
 name = "cookie"
-version = "0.16.2"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
 dependencies = [
  "percent-encoding",
- "time 0.3.20",
+ "time",
  "version_check",
 ]
 
 [[package]]
 name = "core-foundation-sys"
-version = "0.8.4"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.6"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
+checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
 dependencies = [
  "libc",
 ]
 
 [[package]]
-name = "crc32fast"
-version = "1.3.2"
+name = "crc"
+version = "3.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
 dependencies = [
- "cfg-if 1.0.0",
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
+dependencies = [
+ "crossbeam-utils",
 ]
 
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -656,109 +596,106 @@ dependencies = [
 ]
 
 [[package]]
-name = "cxx"
-version = "1.0.94"
+name = "der"
+version = "0.7.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
 dependencies = [
- "cc",
- "cxxbridge-flags",
- "cxxbridge-macro",
- "link-cplusplus",
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
 ]
 
 [[package]]
-name = "cxx-build"
-version = "1.0.94"
+name = "deranged"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
 dependencies = [
- "cc",
- "codespan-reporting",
- "once_cell",
- "proc-macro2",
- "quote",
- "scratch",
- "syn 2.0.13",
+ "powerfmt",
 ]
 
 [[package]]
-name = "cxxbridge-flags"
-version = "1.0.94"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb"
-
-[[package]]
-name = "cxxbridge-macro"
-version = "1.0.94"
+name = "derive_more"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
+checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
 dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.13",
+ "derive_more-impl",
 ]
 
 [[package]]
-name = "derive_more"
-version = "0.99.17"
+name = "derive_more-impl"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
 dependencies = [
  "convert_case",
  "proc-macro2",
  "quote",
- "rustc_version",
- "syn 1.0.109",
+ "syn",
+ "unicode-xid",
 ]
 
 [[package]]
 name = "deunicode"
-version = "0.4.3"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
 
 [[package]]
 name = "digest"
-version = "0.10.6"
+version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
+ "const-oid",
  "crypto-common",
  "subtle",
 ]
 
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
 [[package]]
 name = "either"
-version = "1.8.1"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "email-encoding"
-version = "0.2.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75"
+checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f"
 dependencies = [
- "base64 0.21.0",
+ "base64 0.22.1",
  "memchr",
 ]
 
 [[package]]
 name = "email_address"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112"
-
-[[package]]
-name = "encoding_rs"
-version = "0.8.32"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
-dependencies = [
- "cfg-if 1.0.0",
-]
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
 
 [[package]]
 name = "entities"
@@ -767,68 +704,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
 
 [[package]]
-name = "env_logger"
-version = "0.10.0"
+name = "equivalent"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
-dependencies = [
- "humantime",
- "is-terminal",
- "log",
- "regex",
- "termcolor",
-]
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "errno"
-version = "0.3.0"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
 dependencies = [
- "errno-dragonfly",
  "libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
-name = "errno-dragonfly"
-version = "0.1.2"
+name = "etcetera"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
 dependencies = [
- "cc",
- "libc",
+ "cfg-if 1.0.0",
+ "home",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
-name = "fallible-iterator"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
-
-[[package]]
-name = "fallible-streaming-iterator"
-version = "0.1.9"
+name = "event-listener"
+version = "5.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
 
 [[package]]
 name = "fastrand"
-version = "1.9.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
-dependencies = [
- "instant",
-]
+checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
 
 [[package]]
-name = "flate2"
-version = "1.0.25"
+name = "flume"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
 dependencies = [
- "crc32fast",
- "miniz_oxide",
+ "futures-core",
+ "futures-sink",
+ "spin",
 ]
 
 [[package]]
@@ -839,9 +766,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
 name = "form_urlencoded"
-version = "1.1.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
 dependencies = [
  "percent-encoding",
 ]
@@ -857,26 +784,11 @@ dependencies = [
  "wee_alloc",
 ]
 
-[[package]]
-name = "futures"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
 [[package]]
 name = "futures-channel"
-version = "0.3.28"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -884,15 +796,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.28"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.28"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -900,44 +812,42 @@ dependencies = [
 ]
 
 [[package]]
-name = "futures-io"
-version = "0.3.28"
+name = "futures-intrusive"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
 
 [[package]]
-name = "futures-macro"
-version = "0.3.28"
+name = "futures-io"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.13",
-]
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
 
 [[package]]
 name = "futures-sink"
-version = "0.3.28"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
 
 [[package]]
 name = "futures-task"
-version = "0.3.28"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
 
 [[package]]
 name = "futures-util"
-version = "0.3.28"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
 dependencies = [
- "futures-channel",
  "futures-core",
  "futures-io",
- "futures-macro",
  "futures-sink",
  "futures-task",
  "memchr",
@@ -958,291 +868,458 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.9"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
+
+[[package]]
+name = "hashlink"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "hostname"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
+checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a"
+
+[[package]]
+name = "httparse"
+version = "1.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "hyper"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
 ]
 
 [[package]]
-name = "h2"
-version = "0.3.16"
+name = "hyper-util"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
 dependencies = [
  "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
  "futures-util",
  "http",
- "indexmap",
- "slab",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
  "tokio",
- "tokio-util",
- "tracing",
+ "tower-service",
 ]
 
 [[package]]
-name = "hashbrown"
-version = "0.12.3"
+name = "iana-time-zone"
+version = "0.1.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
 dependencies = [
- "ahash 0.7.6",
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
 ]
 
 [[package]]
-name = "hashlink"
-version = "0.8.1"
+name = "iana-time-zone-haiku"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
 dependencies = [
- "hashbrown",
+ "cc",
 ]
 
 [[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-
-[[package]]
-name = "hermit-abi"
-version = "0.2.6"
+name = "icu_collections"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
 dependencies = [
- "libc",
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
 ]
 
 [[package]]
-name = "hermit-abi"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
-
-[[package]]
-name = "hostname"
-version = "0.3.1"
+name = "icu_locid"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
 dependencies = [
- "libc",
- "match_cfg",
- "winapi",
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
 ]
 
 [[package]]
-name = "http"
-version = "0.2.9"
+name = "icu_locid_transform"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
 dependencies = [
- "bytes",
- "fnv",
- "itoa",
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
 ]
 
 [[package]]
-name = "http-range"
-version = "0.1.5"
+name = "icu_locid_transform_data"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
 
 [[package]]
-name = "httparse"
-version = "1.8.0"
+name = "icu_normalizer"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
 
 [[package]]
-name = "httpdate"
-version = "1.0.2"
+name = "icu_normalizer_data"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
 
 [[package]]
-name = "humansize"
-version = "2.1.3"
+name = "icu_properties"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
 dependencies = [
- "libm",
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
 ]
 
 [[package]]
-name = "humantime"
-version = "2.1.0"
+name = "icu_properties_data"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
 
 [[package]]
-name = "iana-time-zone"
-version = "0.1.56"
+name = "icu_provider"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
 dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "wasm-bindgen",
- "windows",
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
 ]
 
 [[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.1"
+name = "icu_provider_macros"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
 dependencies = [
- "cxx",
- "cxx-build",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "idna"
-version = "0.3.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
 dependencies = [
  "unicode-bidi",
  "unicode-normalization",
 ]
 
 [[package]]
-name = "indexmap"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
-dependencies = [
- "autocfg",
- "hashbrown",
-]
-
-[[package]]
-name = "instant"
-version = "0.1.12"
+name = "idna"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd"
 dependencies = [
- "cfg-if 1.0.0",
+ "icu_normalizer",
+ "icu_properties",
+ "smallvec",
+ "utf8_iter",
 ]
 
 [[package]]
-name = "io-lifetimes"
-version = "1.0.10"
+name = "indexmap"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
 dependencies = [
- "hermit-abi 0.3.1",
- "libc",
- "windows-sys 0.48.0",
+ "equivalent",
+ "hashbrown 0.15.0",
 ]
 
 [[package]]
-name = "is-terminal"
-version = "0.4.7"
+name = "is_terminal_polyfill"
+version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
-dependencies = [
- "hermit-abi 0.3.1",
- "io-lifetimes",
- "rustix",
- "windows-sys 0.48.0",
-]
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
 [[package]]
 name = "itertools"
-version = "0.10.5"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
 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"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
-dependencies = [
- "libc",
-]
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
 name = "js-sys"
-version = "0.3.61"
+version = "0.3.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
 dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "language-tags"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
-
 [[package]]
 name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
 
 [[package]]
 name = "lettre"
-version = "0.10.4"
+version = "0.11.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d"
+checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9"
 dependencies = [
- "base64 0.21.0",
+ "async-trait",
+ "base64 0.22.1",
+ "chumsky",
  "email-encoding",
  "email_address",
  "fastrand",
+ "futures-io",
  "futures-util",
  "hostname",
  "httpdate",
- "idna",
+ "idna 1.0.2",
  "mime",
  "nom",
- "once_cell",
+ "percent-encoding",
  "quoted_printable",
  "rustls",
  "rustls-pemfile",
+ "rustls-pki-types",
  "socket2",
  "tokio",
+ "tokio-rustls",
+ "url",
  "webpki-roots",
 ]
 
 [[package]]
 name = "libc"
-version = "0.2.141"
+version = "0.2.161"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
 
 [[package]]
 name = "libm"
-version = "0.2.6"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
+checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
 
 [[package]]
 name = "libsqlite3-sys"
-version = "0.25.2"
+version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
 dependencies = [
  "cc",
  "pkg-config",
@@ -1250,68 +1327,63 @@ dependencies = [
 ]
 
 [[package]]
-name = "link-cplusplus"
-version = "1.0.8"
+name = "linux-raw-sys"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
-dependencies = [
- "cc",
-]
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
-name = "linux-raw-sys"
-version = "0.3.1"
+name = "litemap"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
+checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
 
 [[package]]
-name = "local-channel"
-version = "0.1.3"
+name = "lock_api"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
 dependencies = [
- "futures-core",
- "futures-sink",
- "futures-util",
- "local-waker",
+ "autocfg",
+ "scopeguard",
 ]
 
 [[package]]
-name = "local-waker"
-version = "0.1.3"
+name = "log"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
 [[package]]
-name = "lock_api"
-version = "0.4.9"
+name = "matchers"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
 dependencies = [
- "autocfg",
- "scopeguard",
+ "regex-automata 0.1.10",
 ]
 
 [[package]]
-name = "log"
-version = "0.4.17"
+name = "matchit"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
-dependencies = [
- "cfg-if 1.0.0",
-]
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
 
 [[package]]
-name = "match_cfg"
-version = "0.1.0"
+name = "md-5"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if 1.0.0",
+ "digest",
+]
 
 [[package]]
 name = "memchr"
-version = "2.5.0"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "memory_units"
@@ -1327,9 +1399,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
 [[package]]
 name = "mime_guess"
-version = "2.0.4"
+version = "2.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
 dependencies = [
  "mime",
  "unicase",
@@ -1343,23 +1415,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.6.2"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
 dependencies = [
- "adler",
+ "adler2",
 ]
 
 [[package]]
 name = "mio"
-version = "0.8.6"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
 dependencies = [
+ "hermit-abi",
  "libc",
- "log",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.45.0",
+ "wasi",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -1372,46 +1444,101 @@ dependencies = [
  "minimal-lexical",
 ]
 
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
 [[package]]
 name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
 version = "0.1.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
 dependencies = [
  "autocfg",
+ "num-integer",
  "num-traits",
 ]
 
 [[package]]
 name = "num-traits"
-version = "0.2.15"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
-name = "num_cpus"
-version = "1.15.0"
+name = "object"
+version = "0.36.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
 dependencies = [
- "hermit-abi 0.2.6",
- "libc",
+ "memchr",
 ]
 
 [[package]]
 name = "once_cell"
-version = "1.17.1"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking"
+version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
 
 [[package]]
 name = "parking_lot"
-version = "0.12.1"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
 dependencies = [
  "lock_api",
  "parking_lot_core",
@@ -1419,15 +1546,15 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.7"
+version = "0.9.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-sys 0.45.0",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -1443,128 +1570,111 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "1.0.12"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
 
 [[package]]
-name = "percent-encoding"
-version = "2.2.0"
+name = "pem-rfc7468"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
 
 [[package]]
-name = "pest"
-version = "2.5.7"
+name = "percent-encoding"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122"
-dependencies = [
- "thiserror",
- "ucd-trie",
-]
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
-name = "pest_derive"
-version = "2.5.7"
+name = "pin-project-lite"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15"
-dependencies = [
- "pest",
- "pest_generator",
-]
+checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
 
 [[package]]
-name = "pest_generator"
-version = "2.5.7"
+name = "pin-utils"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn 2.0.13",
-]
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
-name = "pest_meta"
-version = "2.5.7"
+name = "pkcs1"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
 dependencies = [
- "once_cell",
- "pest",
- "sha2",
+ "der",
+ "pkcs8",
+ "spki",
 ]
 
 [[package]]
-name = "pin-project-lite"
-version = "0.2.9"
+name = "pkcs8"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
 
 [[package]]
-name = "pin-utils"
-version = "0.1.0"
+name = "pkg-config"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
 
 [[package]]
-name = "pkg-config"
-version = "0.3.26"
+name = "powerfmt"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.17"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.56"
+version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
-name = "quote"
-version = "1.0.26"
+name = "psm"
+version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205"
 dependencies = [
- "proc-macro2",
+ "cc",
 ]
 
 [[package]]
-name = "quoted_printable"
-version = "0.4.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6"
-
-[[package]]
-name = "r2d2"
-version = "0.8.10"
+name = "quote"
+version = "1.0.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
 dependencies = [
- "log",
- "parking_lot",
- "scheduled-thread-pool",
+ "proc-macro2",
 ]
 
 [[package]]
-name = "r2d2_sqlite"
-version = "0.21.0"
+name = "quoted_printable"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4f5d0337e99cd5cacd91ffc326c6cc9d8078def459df560c4f9bf9ba4a51034"
-dependencies = [
- "r2d2",
- "rusqlite",
-]
+checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
 
 [[package]]
 name = "rand"
@@ -1600,47 +1710,69 @@ dependencies = [
 name = "recipes"
 version = "1.0.0"
 dependencies = [
- "actix-files",
- "actix-web",
  "argon2",
  "askama",
- "askama_actix",
+ "askama_axum",
+ "axum",
+ "axum-extra",
  "chrono",
  "clap",
  "common",
  "derive_more",
- "env_logger",
- "futures",
  "itertools",
  "lettre",
- "log",
- "r2d2",
- "r2d2_sqlite",
  "rand",
  "rand_core",
  "ron",
- "rusqlite",
  "serde",
+ "sqlx",
+ "thiserror",
+ "tokio",
+ "tower",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.8",
+ "regex-syntax 0.8.5",
 ]
 
 [[package]]
-name = "redox_syscall"
-version = "0.2.16"
+name = "regex-automata"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
 dependencies = [
- "bitflags",
+ "regex-syntax 0.6.29",
 ]
 
 [[package]]
-name = "regex"
-version = "1.7.3"
+name = "regex-automata"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
+checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax",
+ "regex-syntax 0.8.5",
 ]
 
 [[package]]
@@ -1649,175 +1781,179 @@ version = "0.6.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
 [[package]]
 name = "ring"
-version = "0.16.20"
+version = "0.17.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
  "cc",
+ "cfg-if 1.0.0",
+ "getrandom",
  "libc",
- "once_cell",
  "spin",
  "untrusted",
- "web-sys",
- "winapi",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "ron"
-version = "0.8.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
 dependencies = [
- "base64 0.13.1",
+ "base64 0.21.7",
  "bitflags",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
-name = "rusqlite"
-version = "0.28.0"
+name = "rsa"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
 dependencies = [
- "bitflags",
- "chrono",
- "fallible-iterator",
- "fallible-streaming-iterator",
- "hashlink",
- "libsqlite3-sys",
- "smallvec",
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
 ]
 
 [[package]]
-name = "rustc_version"
-version = "0.4.0"
+name = "rustc-demangle"
+version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
-dependencies = [
- "semver",
-]
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
 
 [[package]]
 name = "rustix"
-version = "0.37.8"
+version = "0.38.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aef160324be24d31a62147fae491c14d2204a3865c7ca8c3b0d7f7bcb3ea635"
+checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
 dependencies = [
  "bitflags",
  "errno",
- "io-lifetimes",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "rustls"
-version = "0.21.0"
+version = "0.23.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d"
+checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
 dependencies = [
  "log",
+ "once_cell",
  "ring",
+ "rustls-pki-types",
  "rustls-webpki",
- "sct",
+ "subtle",
+ "zeroize",
 ]
 
 [[package]]
 name = "rustls-pemfile"
-version = "1.0.2"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
 dependencies = [
- "base64 0.21.0",
+ "rustls-pki-types",
 ]
 
+[[package]]
+name = "rustls-pki-types"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
+
 [[package]]
 name = "rustls-webpki"
-version = "0.100.1"
+version = "0.102.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
+checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
 dependencies = [
  "ring",
+ "rustls-pki-types",
  "untrusted",
 ]
 
 [[package]]
-name = "ryu"
-version = "1.0.13"
+name = "rustversion"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
 
 [[package]]
-name = "scheduled-thread-pool"
-version = "0.2.7"
+name = "ryu"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
-dependencies = [
- "parking_lot",
-]
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
 name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
-[[package]]
-name = "scratch"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
-
-[[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"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
 name = "serde"
-version = "1.0.159"
+version = "1.0.214"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
+checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.159"
+version = "1.0.214"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
+checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.95"
+version = "1.0.132"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
 dependencies = [
  "itoa",
+ "memchr",
  "ryu",
  "serde",
 ]
 
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
 [[package]]
 name = "serde_urlencoded"
 version = "0.7.1"
@@ -1832,9 +1968,9 @@ dependencies = [
 
 [[package]]
 name = "sha1"
-version = "0.10.5"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
 dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
@@ -1843,81 +1979,361 @@ dependencies = [
 
 [[package]]
 name = "sha2"
-version = "0.10.6"
+version = "0.10.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
 dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
  "digest",
 ]
 
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.1"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
 dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
 [[package]]
 name = "slab"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
 dependencies = [
  "autocfg",
 ]
 
 [[package]]
 name = "slug"
-version = "0.1.4"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
 dependencies = [
  "deunicode",
+ "wasm-bindgen",
 ]
 
 [[package]]
 name = "smallvec"
-version = "1.10.0"
+version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "socket2"
-version = "0.4.9"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
 dependencies = [
  "libc",
- "winapi",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "spin"
-version = "0.5.2"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlformat"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
+dependencies = [
+ "nom",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
+dependencies = [
+ "atoi",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.14.5",
+ "hashlink",
+ "hex",
+ "indexmap",
+ "log",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags",
+ "byteorder",
+ "chrono",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
+dependencies = [
+ "atoi",
+ "chrono",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "stacker"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b"
+dependencies = [
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "psm",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
 
 [[package]]
 name = "strsim"
-version = "0.10.0"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
 name = "subtle"
-version = "2.4.1"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
 [[package]]
 name = "syn"
-version = "1.0.109"
+version = "2.0.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1925,63 +2341,81 @@ dependencies = [
 ]
 
 [[package]]
-name = "syn"
-version = "2.0.13"
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
+[[package]]
+name = "synstructure"
+version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
 dependencies = [
  "proc-macro2",
  "quote",
- "unicode-ident",
+ "syn",
 ]
 
 [[package]]
-name = "termcolor"
-version = "1.2.0"
+name = "tempfile"
+version = "3.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
 dependencies = [
- "winapi-util",
+ "cfg-if 1.0.0",
+ "fastrand",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "thiserror"
-version = "1.0.40"
+version = "1.0.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.40"
+version = "1.0.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.13",
+ "syn",
 ]
 
 [[package]]
-name = "time"
-version = "0.1.45"
+name = "thread_local"
+version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
 dependencies = [
- "libc",
- "wasi 0.10.0+wasi-snapshot-preview1",
- "winapi",
+ "cfg-if 1.0.0",
+ "once_cell",
 ]
 
 [[package]]
 name = "time"
-version = "0.3.20"
+version = "0.3.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
+checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
 dependencies = [
+ "deranged",
  "itoa",
+ "num-conv",
+ "powerfmt",
  "serde",
  "time-core",
  "time-macros",
@@ -1989,24 +2423,35 @@ dependencies = [
 
 [[package]]
 name = "time-core"
-version = "0.1.0"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
 
 [[package]]
 name = "time-macros"
-version = "0.2.8"
+version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
+checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
 dependencies = [
+ "num-conv",
  "time-core",
 ]
 
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
 [[package]]
 name = "tinyvec"
-version = "1.6.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2019,11 +2464,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.27.0"
+version = "1.41.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
+checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
 dependencies = [
- "autocfg",
+ "backtrace",
  "bytes",
  "libc",
  "mio",
@@ -2031,42 +2476,169 @@ dependencies = [
  "pin-project-lite",
  "signal-hook-registry",
  "socket2",
- "windows-sys 0.45.0",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+dependencies = [
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
 ]
 
 [[package]]
 name = "tokio-util"
-version = "0.7.7"
+version = "0.7.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
+checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
 dependencies = [
  "bytes",
  "futures-core",
  "futures-sink",
  "pin-project-lite",
  "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper 0.1.2",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
  "tracing",
 ]
 
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
 [[package]]
 name = "tracing"
-version = "0.1.37"
+version = "0.1.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
 dependencies = [
- "cfg-if 1.0.0",
  "log",
  "pin-project-lite",
+ "tracing-attributes",
  "tracing-core",
 ]
 
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "tracing-core"
-version = "0.1.30"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
 dependencies = [
+ "matchers",
+ "nu-ansi-term",
  "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
 ]
 
 [[package]]
@@ -2077,51 +2649,54 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
 
 [[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"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
 [[package]]
 name = "unicase"
-version = "2.6.0"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
-dependencies = [
- "version_check",
-]
+checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
 
 [[package]]
 name = "unicode-bidi"
-version = "0.3.13"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.8"
+version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
 
 [[package]]
 name = "unicode-normalization"
-version = "0.1.22"
+version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
 dependencies = [
  "tinyvec",
 ]
 
 [[package]]
-name = "unicode-width"
-version = "0.1.10"
+name = "unicode-properties"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
 [[package]]
 name = "unicode_categories"
@@ -2131,26 +2706,44 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
 [[package]]
 name = "untrusted"
-version = "0.7.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 
 [[package]]
 name = "url"
-version = "2.3.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
 dependencies = [
  "form_urlencoded",
- "idna",
+ "idna 0.5.0",
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
 [[package]]
 name = "utf8parse"
-version = "0.2.1"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
 [[package]]
 name = "vcpkg"
@@ -2160,52 +2753,53 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
 name = "version_check"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
 [[package]]
 name = "wasi"
-version = "0.10.0+wasi-snapshot-preview1"
+version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
-name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+name = "wasite"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.84"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
 dependencies = [
  "cfg-if 1.0.0",
+ "once_cell",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.84"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
 dependencies = [
  "bumpalo",
  "log",
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.84"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -2213,28 +2807,28 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.84"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.84"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
 
 [[package]]
 name = "web-sys"
-version = "0.3.61"
+version = "0.3.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -2242,11 +2836,11 @@ dependencies = [
 
 [[package]]
 name = "webpki-roots"
-version = "0.23.0"
+version = "0.26.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125"
+checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
 dependencies = [
- "rustls-webpki",
+ "rustls-pki-types",
 ]
 
 [[package]]
@@ -2261,6 +2855,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "whoami"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
+dependencies = [
+ "redox_syscall",
+ "wasite",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -2278,187 +2882,280 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
-name = "winapi-util"
-version = "0.1.5"
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
 dependencies = [
- "winapi",
+ "windows-core",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
+name = "windows-core"
+version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.6",
+]
 
 [[package]]
-name = "windows"
+name = "windows-sys"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 dependencies = [
- "windows-targets 0.48.0",
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
 name = "windows-sys"
-version = "0.45.0"
+version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.42.2",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
 name = "windows-sys"
-version = "0.48.0"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
 dependencies = [
- "windows-targets 0.48.0",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
 name = "windows-targets"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
 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",
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
 ]
 
 [[package]]
 name = "windows-targets"
-version = "0.48.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 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",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.48.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.48.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.48.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.48.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.48.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.48.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.42.2"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.48.0"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
 
 [[package]]
-name = "zstd"
-version = "0.12.3+zstd.1.5.2"
+name = "yoke"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806"
+checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
 dependencies = [
- "zstd-safe",
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
 ]
 
 [[package]]
-name = "zstd-safe"
-version = "6.0.5+zstd.1.5.4"
+name = "yoke-derive"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b"
+checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
 dependencies = [
- "libc",
- "zstd-sys",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
 ]
 
 [[package]]
-name = "zstd-sys"
-version = "2.0.8+zstd.1.5.5"
+name = "zerocopy"
+version = "0.7.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 dependencies = [
- "cc",
- "libc",
- "pkg-config",
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
index 1686e30..ffc5e91 100644 (file)
@@ -1,10 +1,8 @@
 [workspace]\r
 \r
-members = [\r
-    "backend",\r
-    "frontend",\r
-    "common",\r
-]\r
+resolver = "2"\r
+\r
+members = ["backend", "frontend", "common"]\r
 \r
 [profile.release]\r
 strip = true\r
index cb02d2c..338aba4 100644 (file)
--- a/README.md
+++ b/README.md
@@ -46,7 +46,6 @@ To launch node run 'npm run start' in 'frontend/www' directory
 ## Useful URLs
 
 * Rust patterns : https://github.com/rust-unofficial/patterns/tree/master/patterns
-* Rusqlite (SQLite) : https://docs.rs/rusqlite/0.20.0/rusqlite/
 * Node install: https://nodejs.org/en/download/
 
 
diff --git a/TODO.md b/TODO.md
index 7fb8595..b3186cc 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -1,20 +1,29 @@
+* Clean the old code + commit
+* How to log error to journalctl or elsewhere + debug log?
 * Try using WASM for all the client logic (test on editing/creating a recipe)
     * Understand the example here:
         * https://github.com/rustwasm/wasm-bindgen/tree/main/examples/todomvc -> https://rustwasm.github.io/wasm-bindgen/exbuild/todomvc/#/
-* Describe the use cases.
-* Define the UI (mockups).
-    * Two CSS: one for desktop and one for mobile
-    * Use CSS flex/grid to define a good design/layout
-* Define the logic behind each page and action.
+* Add a level of severity for the message template, use error severity in "impl axum::response::IntoResponse for db::DBError"
+* Review the recipe model (SQL)
+* Describe the use cases in details.
+    * Define the UI (mockups).
+        * Two CSS: one for desktop and one for mobile
+        * Use CSS flex/grid to define a good design/layout
+    * Define the logic behind each page and action.
+* Implement:
+    .service(services::edit_recipe)
+    .service(services::new_recipe)
+    .service(services::webapi::set_recipe_title)
+    .service(services::webapi::set_recipe_description)
 * Add support to translations into db model.
 
+[ok] Reactivate sign up/in/out
 [ok] Change all id to i64
 [ok] Check cookie lifetime -> Session by default
 [ok] Asynchonous email sending and database requests
     [ok] Try to return Result for async routes (and watch what is printed in log)
     [ok] Then try to make async database calls
     [ok] Set email sending as async and show a waiter when sending email. Handle (and test) a timeout (~10s). -> (timeout put to 60s)
-[ok] How to log error to journalctl?
 [ok] Sign out
 [ok] Read all the askama doc and see if the current approach is good
 [ok] Handle 404
index d78c385..a6c5bdf 100644 (file)
@@ -5,36 +5,48 @@ authors = ["Grégory Burri <greg.burri@gmail.com>"]
 edition = "2021"
 
 [dependencies]
-common = {path = "../common"}
+common = { path = "../common" }
 
-actix-web = "4"
-actix-files = "0.6"
+axum = { version = "0.7", features = ["macros"] }
+axum-extra = { version = "0.9", features = ["cookie"] }
+tokio = { version = "1", features = ["full"] }
+tower = { version = "0.5", features = ["util"] }
+tower-http = { version = "0.6", features = ["fs", "trace"] }
 
-chrono = "0.4"
-
-ron = "0.8" # Rust object notation, to load configuration files.
-serde = {version = "1.0", features = ["derive"]}
-
-itertools = "0.10"
-clap = {version = "4", features = ["derive"]}
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 
-log = "0.4"
-env_logger = "0.10"
+chrono = "0.4"
 
-r2d2_sqlite = "0.21" # Connection pool with rusqlite (SQLite access).
-r2d2 = "0.8"
-rusqlite = {version = "0.28", features = ["bundled", "chrono"]}
+# Rust object notation, to load configuration files.
+ron = "0.8"
+serde = { version = "1.0", features = ["derive"] }
 
-futures = "0.3" # Needed by askam with the feature 'with-actix-web'.
+itertools = "0.13"
+clap = { version = "4", features = ["derive"] }
 
-askama = {version = "0.12", features = ["with-actix-web", "mime", "mime_guess", "markdown"]}
-askama_actix = "0.14"
+sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
 
-argon2 = {version = "0.5", features = ["default", "std"]}
-rand_core = {version = "0.6", features = ["std"]}
+askama = { version = "0.12", features = [
+    "with-axum",
+    "mime",
+    "mime_guess",
+    "markdown",
+] }
+askama_axum = "0.4"
 
+argon2 = { version = "0.5", features = ["default", "std"] }
+rand_core = { version = "0.6", features = ["std"] }
 rand = "0.8"
 
-lettre = {version = "0.10", default-features = false, features = ["smtp-transport", "pool", "hostname", "builder", "rustls-tls"]}
-
-derive_more = "0.99"
\ No newline at end of file
+lettre = { version = "0.11", default-features = false, features = [
+    "smtp-transport",
+    "pool",
+    "hostname",
+    "builder",
+    "tokio1",
+    "tokio1-rustls-tls",
+] }
+
+derive_more = { version = "1", features = ["full"] }
+thiserror = "1"
index 24af485..59ee30c 100644 (file)
@@ -1,22 +1,38 @@
 use std::{fmt, fs::File};
 
-use ron::de::from_reader;
-use serde::Deserialize;
+use ron::{
+    de::from_reader,
+    ser::{to_writer_pretty, PrettyConfig},
+};
+use serde::{Deserialize, Serialize};
 
 use crate::consts;
 
-#[derive(Deserialize, Clone)]
+#[derive(Deserialize, Serialize, Clone)]
 pub struct Config {
     pub port: u16,
+    pub smtp_relay_address: String,
     pub smtp_login: String,
     pub smtp_password: String,
 }
 
+impl Config {
+    pub fn default() -> Self {
+        Config {
+            port: 8082,
+            smtp_relay_address: "mail.something.com".to_string(),
+            smtp_login: "login".to_string(),
+            smtp_password: "password".to_string(),
+        }
+    }
+}
+
 // Avoid to print passwords.
 impl fmt::Debug for Config {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("Config")
             .field("port", &self.port)
+            .field("smtp_relay_address", &self.smtp_relay_address)
             .field("smtp_login", &self.smtp_login)
             .field("smtp_password", &"*****")
             .finish()
@@ -24,10 +40,25 @@ impl fmt::Debug for Config {
 }
 
 pub fn load() -> Config {
-    let f = File::open(consts::FILE_CONF)
-        .unwrap_or_else(|_| panic!("Failed to open configuration file {}", consts::FILE_CONF));
-    match from_reader(f) {
-        Ok(c) => c,
-        Err(e) => panic!("Failed to load config: {}", e),
+    match File::open(consts::FILE_CONF) {
+        Ok(file) => from_reader(file).expect(&format!(
+            "Failed to open configuration file {}",
+            consts::FILE_CONF
+        )),
+        Err(_) => {
+            let file = File::create(consts::FILE_CONF).expect(&format!(
+                "Failed to create default configuration file {}",
+                consts::FILE_CONF
+            ));
+
+            let default_config = Config::default();
+
+            to_writer_pretty(file, &default_config, PrettyConfig::new()).expect(&format!(
+                "Failed to write default configuration file {}",
+                consts::FILE_CONF
+            ));
+
+            default_config
+        }
     }
 }
index 45167a3..3fff9d6 100644 (file)
@@ -5,7 +5,13 @@ pub const DB_DIRECTORY: &str = "data";
 pub const DB_FILENAME: &str = "recipes.sqlite";
 pub const SQL_FILENAME: &str = "sql/version_{VERSION}.sql";
 pub const VALIDATION_TOKEN_DURATION: i64 = 1 * 60 * 60; // 1 hour. [s].
-pub const REVERSE_PROXY_IP_HTTP_FIELD: &str = "x-real-ip";
 pub const COOKIE_AUTH_TOKEN_NAME: &str = "auth_token";
-pub const AUTHENTICATION_TOKEN_SIZE: usize = 32; // Number of alphanumeric characters for cookie authentication token.
+
+// Number of alphanumeric characters for cookie authentication token.
+pub const AUTHENTICATION_TOKEN_SIZE: usize = 32;
+
 pub const SEND_EMAIL_TIMEOUT: Duration = Duration::from_secs(60);
+
+// HTTP headers, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers.
+// Common headers can be found in 'axum::http::header' (which is a re-export of the create 'http').
+pub const REVERSE_PROXY_IP_HTTP_FIELD: &str = "x-real-ip"; // Set by the reverse proxy (Nginx).
diff --git a/backend/src/data/asynchronous.rs b/backend/src/data/asynchronous.rs
deleted file mode 100644 (file)
index 5d27388..0000000
+++ /dev/null
@@ -1,195 +0,0 @@
-//! Functions to be called from actix code. They are asynchonrous and won't block worker thread caller.
-
-use std::fmt;
-
-use actix_web::{error::BlockingError, web};
-use chrono::{prelude::*, Duration};
-
-use super::db::*;
-use crate::model;
-
-#[derive(Debug)]
-pub enum DBAsyncError {
-    DBError(DBError),
-    ActixError(BlockingError),
-    Other(String),
-}
-
-impl fmt::Display for DBAsyncError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
-        write!(f, "{:?}", self)
-    }
-}
-
-impl std::error::Error for DBAsyncError {}
-
-impl From<DBError> for DBAsyncError {
-    fn from(error: DBError) -> Self {
-        DBAsyncError::DBError(error)
-    }
-}
-
-impl From<BlockingError> for DBAsyncError {
-    fn from(error: BlockingError) -> Self {
-        DBAsyncError::ActixError(error)
-    }
-}
-
-impl DBAsyncError {
-    fn from_dyn_error(error: Box<dyn std::error::Error>) -> Self {
-        DBAsyncError::Other(error.to_string())
-    }
-}
-
-fn combine_errors<T>(
-    error: std::result::Result<std::result::Result<T, DBAsyncError>, BlockingError>,
-) -> Result<T> {
-    error?
-}
-
-type Result<T> = std::result::Result<T, DBAsyncError>;
-
-impl Connection {
-    pub async fn get_all_recipe_titles_async(&self) -> Result<Vec<(i64, String)>> {
-        let self_copy = self.clone();
-        web::block(move || self_copy.get_all_recipe_titles().unwrap_or_default())
-            .await
-            .map_err(DBAsyncError::from)
-    }
-
-    pub async fn get_recipe_async(&self, id: i64) -> Result<model::Recipe> {
-        let self_copy = self.clone();
-        combine_errors(
-            web::block(move || self_copy.get_recipe(id).map_err(DBAsyncError::from)).await,
-        )
-    }
-
-    pub async fn load_user_async(&self, user_id: i64) -> Result<model::User> {
-        let self_copy = self.clone();
-        combine_errors(
-            web::block(move || self_copy.load_user(user_id).map_err(DBAsyncError::from)).await,
-        )
-    }
-
-    pub async fn sign_up_async(&self, email: &str, password: &str) -> Result<SignUpResult> {
-        let self_copy = self.clone();
-        let email_copy = email.to_string();
-        let password_copy = password.to_string();
-        combine_errors(
-            web::block(move || {
-                self_copy
-                    .sign_up(&email_copy, &password_copy)
-                    .map_err(DBAsyncError::from)
-            })
-            .await,
-        )
-    }
-
-    pub async fn validation_async(
-        &self,
-        token: &str,
-        validation_time: Duration,
-        ip: &str,
-        user_agent: &str,
-    ) -> Result<ValidationResult> {
-        let self_copy = self.clone();
-        let token_copy = token.to_string();
-        let ip_copy = ip.to_string();
-        let user_agent_copy = user_agent.to_string();
-        combine_errors(
-            web::block(move || {
-                self_copy
-                    .validation(&token_copy, validation_time, &ip_copy, &user_agent_copy)
-                    .map_err(DBAsyncError::from)
-            })
-            .await,
-        )
-    }
-
-    pub async fn sign_in_async(
-        &self,
-        email: &str,
-        password: &str,
-        ip: &str,
-        user_agent: &str,
-    ) -> Result<SignInResult> {
-        let self_copy = self.clone();
-        let email_copy = email.to_string();
-        let password_copy = password.to_string();
-        let ip_copy = ip.to_string();
-        let user_agent_copy = user_agent.to_string();
-        combine_errors(
-            web::block(move || {
-                self_copy
-                    .sign_in(&email_copy, &password_copy, &ip_copy, &user_agent_copy)
-                    .map_err(DBAsyncError::from)
-            })
-            .await,
-        )
-    }
-
-    pub async fn authentication_async(
-        &self,
-        token: &str,
-        ip: &str,
-        user_agent: &str,
-    ) -> Result<AuthenticationResult> {
-        let self_copy = self.clone();
-        let token_copy = token.to_string();
-        let ip_copy = ip.to_string();
-        let user_agent_copy = user_agent.to_string();
-        combine_errors(
-            web::block(move || {
-                self_copy
-                    .authentication(&token_copy, &ip_copy, &user_agent_copy)
-                    .map_err(DBAsyncError::from)
-            })
-            .await,
-        )
-    }
-
-    pub async fn sign_out_async(&self, token: &str) -> Result<()> {
-        let self_copy = self.clone();
-        let token_copy = token.to_string();
-        combine_errors(
-            web::block(move || self_copy.sign_out(&token_copy).map_err(DBAsyncError::from)).await,
-        )
-    }
-
-    pub async fn create_recipe_async(&self, user_id: i64) -> Result<i64> {
-        let self_copy = self.clone();
-        combine_errors(
-            web::block(move || self_copy.create_recipe(user_id).map_err(DBAsyncError::from)).await,
-        )
-    }
-
-    pub async fn set_recipe_title_async(&self, recipe_id: i64, title: &str) -> Result<()> {
-        let self_copy = self.clone();
-        let title_copy = title.to_string();
-        combine_errors(
-            web::block(move || {
-                self_copy
-                    .set_recipe_title(recipe_id, &title_copy)
-                    .map_err(DBAsyncError::from)
-            })
-            .await,
-        )
-    }
-
-    pub async fn set_recipe_description_async(
-        &self,
-        recipe_id: i64,
-        description: &str,
-    ) -> Result<()> {
-        let self_copy = self.clone();
-        let description_copy = description.to_string();
-        combine_errors(
-            web::block(move || {
-                self_copy
-                    .set_recipe_description(recipe_id, &description_copy)
-                    .map_err(DBAsyncError::from)
-            })
-            .await,
-        )
-    }
-}
index 5c925e1..c84f72b 100644 (file)
@@ -3,14 +3,16 @@ use std::{
     fs::{self, File},
     io::Read,
     path::Path,
+    str::FromStr,
 };
 
 use chrono::{prelude::*, Duration};
-use itertools::Itertools;
-use r2d2::{Pool, PooledConnection};
-use r2d2_sqlite::SqliteConnectionManager;
 use rand::distributions::{Alphanumeric, DistString};
-use rusqlite::{named_params, params, OptionalExtension, Params};
+use sqlx::{
+    sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
+    Pool, Sqlite, Transaction,
+};
+use thiserror::Error;
 
 use crate::{
     consts,
@@ -20,32 +22,19 @@ use crate::{
 
 const CURRENT_DB_VERSION: u32 = 1;
 
-#[derive(Debug)]
+#[derive(Error, Debug)]
 pub enum DBError {
-    SqliteError(rusqlite::Error),
-    R2d2Error(r2d2::Error),
-    UnsupportedVersion(u32),
-    Other(String),
-}
+    #[error("Sqlx error: {0}")]
+    Sqlx(#[from] sqlx::Error),
 
-impl fmt::Display for DBError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
-        write!(f, "{:?}", self)
-    }
-}
-
-impl std::error::Error for DBError {}
-
-impl From<rusqlite::Error> for DBError {
-    fn from(error: rusqlite::Error) -> Self {
-        DBError::SqliteError(error)
-    }
-}
+    #[error(
+        "Unsupported database version: {0} (code version: {})",
+        CURRENT_DB_VERSION
+    )]
+    UnsupportedVersion(u32),
 
-impl From<r2d2::Error> for DBError {
-    fn from(error: r2d2::Error) -> Self {
-        DBError::R2d2Error(error)
-    }
+    #[error("Unknown error: {0}")]
+    Other(String),
 }
 
 impl DBError {
@@ -85,92 +74,99 @@ pub enum AuthenticationResult {
 
 #[derive(Clone)]
 pub struct Connection {
-    pool: Pool<SqliteConnectionManager>,
+    pool: Pool<Sqlite>,
 }
 
 impl Connection {
-    pub fn new() -> Result<Connection> {
+    pub async fn new() -> Result<Connection> {
         let path = Path::new(consts::DB_DIRECTORY).join(consts::DB_FILENAME);
-        Self::new_from_file(path)
+        Self::new_from_file(path).await
     }
 
-    pub fn new_in_memory() -> Result<Connection> {
-        Self::create_connection(SqliteConnectionManager::memory())
+    // For tests.
+    #[warn(dead_code)]
+    pub async fn new_in_memory() -> Result<Connection> {
+        Self::create_connection(SqlitePoolOptions::new().connect("sqlite::memory:").await?).await
     }
 
-    pub fn new_from_file<P: AsRef<Path>>(file: P) -> Result<Connection> {
+    pub async fn new_from_file<P: AsRef<Path>>(file: P) -> Result<Connection> {
         if let Some(data_dir) = file.as_ref().parent() {
             if !data_dir.exists() {
                 fs::DirBuilder::new().create(data_dir).unwrap();
             }
         }
 
-        Self::create_connection(SqliteConnectionManager::file(file))
+        let options = SqliteConnectOptions::from_str(&format!(
+            "sqlite://{}",
+            file.as_ref().to_str().unwrap()
+        ))?
+        .journal_mode(SqliteJournalMode::Wal) // TODO: use 'Wal2' when available.
+        .create_if_missing(true)
+        .pragma("foreign_keys", "ON")
+        .pragma("synchronous", "NORMAL");
+
+        Self::create_connection(SqlitePoolOptions::new().connect_with(options).await?).await
     }
 
-    fn create_connection(manager: SqliteConnectionManager) -> Result<Connection> {
-        let pool = r2d2::Pool::new(manager).unwrap();
+    async fn create_connection(pool: Pool<Sqlite>) -> Result<Connection> {
         let connection = Connection { pool };
-        connection.create_or_update_db()?;
+        connection.create_or_update_db().await?;
         Ok(connection)
     }
 
-    fn get(&self) -> Result<PooledConnection<SqliteConnectionManager>> {
-        let con = self.pool.get()?;
-        // ('foreign_keys' is ON by default).
-        con.pragma_update(None, "synchronous", "NORMAL")?;
-        Ok(con)
+    async fn tx(&self) -> Result<Transaction<Sqlite>> {
+        self.pool.begin().await.map_err(DBError::from)
     }
 
     /// Called after the connection has been established for creating or updating the database.
     /// The 'Version' table tracks the current state of the database.
-    fn create_or_update_db(&self) -> Result<()> {
-        let mut con = self.get()?;
-        con.pragma_update(None, "journal_mode", "WAL")?; // Note: use "WAL2" when available.
-
-        let tx = con.transaction()?;
+    async fn create_or_update_db(&self) -> Result<()> {
+        let mut tx = self.tx().await?; //con.transaction()?;
 
         // Check current database version. (Version 0 corresponds to an empty database).
-        let mut version = {
-            match tx.query_row(
-                "SELECT [name] FROM [sqlite_master] WHERE [type] = 'table' AND [name] = 'Version'",
-                [],
-                |row| row.get::<usize, String>(0),
-            ) {
-                Ok(_) => tx
-                    .query_row(
-                        "SELECT [version] FROM [Version] ORDER BY [id] DESC",
-                        [],
-                        |row| row.get(0),
-                    )
-                    .unwrap_or_default(),
-                Err(_) => 0,
-            }
+        let mut version = match sqlx::query(
+            r#"
+SELECT [name] FROM [sqlite_master]
+WHERE [type] = 'table' AND [name] = 'Version'
+            "#,
+        )
+        .fetch_one(&mut *tx)
+        .await
+        {
+            Ok(_) => sqlx::query_scalar("SELECT [version] FROM [Version] ORDER BY [id] DESC")
+                .fetch_optional(&mut *tx)
+                .await?
+                .unwrap_or(0),
+            Err(_) => 0, // If the database doesn't exist.
         };
 
-        while Self::update_to_next_version(version, &tx)? {
+        while Self::update_to_next_version(version, &mut tx).await? {
             version += 1;
         }
 
-        tx.commit()?;
+        tx.commit().await?;
 
         Ok(())
     }
 
-    fn update_to_next_version(current_version: u32, tx: &rusqlite::Transaction) -> Result<bool> {
+    async fn update_to_next_version(
+        current_version: u32,
+        tx: &mut Transaction<'_, Sqlite>,
+    ) -> Result<bool> {
         let next_version = current_version + 1;
 
         if next_version <= CURRENT_DB_VERSION {
             println!("Update to version {}...", next_version);
         }
 
-        fn update_version(to_version: u32, tx: &rusqlite::Transaction) -> Result<()> {
-            tx.execute(
-                "INSERT INTO [Version] ([version], [datetime]) VALUES (?1, datetime('now'))",
-                [to_version],
+        async fn update_version(to_version: u32, tx: &mut Transaction<'_, Sqlite>) -> Result<()> {
+            sqlx::query(
+                "INSERT INTO [Version] ([version], [datetime]) VALUES ($1, datetime('now'))",
             )
-            .map(|_| ())
-            .map_err(DBError::from)
+            .bind(to_version)
+            .execute(&mut **tx)
+            .await?;
+            Ok(())
         }
 
         fn ok(updated: bool) -> Result<bool> {
@@ -183,109 +179,86 @@ impl Connection {
         match next_version {
             1 => {
                 let sql_file = consts::SQL_FILENAME.replace("{VERSION}", &next_version.to_string());
-                tx.execute_batch(&load_sql_file(&sql_file)?)?;
-                update_version(next_version, tx)?;
+                sqlx::query(&load_sql_file(&sql_file)?)
+                    .execute(&mut **tx)
+                    .await?;
+                update_version(next_version, tx).await?;
 
                 ok(true)
             }
 
-            // Version 1 doesn't exist yet.
+            // Version 2 doesn't exist yet.
             2 => ok(false),
 
             v => Err(DBError::UnsupportedVersion(v)),
         }
     }
 
-    pub fn get_all_recipe_titles(&self) -> Result<Vec<(i64, String)>> {
-        let con = self.get()?;
-
-        let mut stmt = con.prepare("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")?;
-
-        let titles: std::result::Result<Vec<(i64, String)>, rusqlite::Error> = stmt
-            .query_map([], |row| Ok((row.get("id")?, row.get("title")?)))?
-            .collect();
-
-        titles.map_err(DBError::from)
-    }
-
-    /* Not used for the moment.
-    pub fn get_all_recipes(&self) -> Result<Vec<model::Recipe>> {
-        let con = self.get()?;
-        let mut stmt = con.prepare("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")?;
-        let recipes =
-            stmt.query_map([], |row| {
-                Ok(model::Recipe::new(row.get(0)?, row.get(1)?))
-            })?.map(|r| r.unwrap()).collect_vec(); // TODO: remove unwrap.
-        Ok(recipes)
-    } */
-
-    pub fn get_recipe(&self, id: i64) -> Result<model::Recipe> {
-        let con = self.get()?;
-        con.query_row(
-            "SELECT [id], [user_id], [title], [description] FROM [Recipe] WHERE [id] = ?1",
-            [id],
-            |row| {
-                Ok(model::Recipe::new(
-                    row.get("id")?,
-                    row.get("user_id")?,
-                    row.get("title")?,
-                    row.get("description")?,
-                ))
-            },
+    pub async fn get_all_recipe_titles(&self) -> Result<Vec<(i64, String)>> {
+        sqlx::query_as("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")
+            .fetch_all(&self.pool)
+            .await
+            .map_err(DBError::from)
+    }
+
+    pub async fn get_recipe(&self, id: i64) -> Result<Option<model::Recipe>> {
+        sqlx::query_as(
+            r#"
+SELECT [id], [user_id], [title], [description]
+FROM [Recipe] WHERE [id] = $1
+            "#,
         )
+        .bind(id)
+        .fetch_optional(&self.pool)
+        .await
         .map_err(DBError::from)
     }
 
-    pub fn get_user_login_info(&self, token: &str) -> Result<model::UserLoginInfo> {
-        let con = self.get()?;
-        con.query_row("SELECT [last_login_datetime], [ip], [user_agent] FROM [UserLoginToken] WHERE [token] = ?1", [token], |r| {
-            Ok(model::UserLoginInfo {
-                last_login_datetime: r.get("last_login_datetime")?,
-                ip: r.get("ip")?,
-                user_agent: r.get("user_agent")?,
-            })
-        }).map_err(DBError::from)
-    }
-
-    pub fn load_user(&self, user_id: i64) -> Result<model::User> {
-        let con = self.get()?;
-        con.query_row(
-            "SELECT [email] FROM [User] WHERE [id] = ?1",
-            [user_id],
-            |r| {
-                Ok(model::User {
-                    id: user_id,
-                    email: r.get("email")?,
-                })
-            },
+    // For tests.
+    #[warn(dead_code)]
+    pub async fn get_user_login_info(&self, token: &str) -> Result<model::UserLoginInfo> {
+        sqlx::query_as(
+            r#"
+SELECT [last_login_datetime], [ip], [user_agent]
+FROM [UserLoginToken] WHERE [token] = $1
+            "#,
         )
+        .bind(token)
+        .fetch_one(&self.pool)
+        .await
         .map_err(DBError::from)
     }
 
-    pub fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResult> {
+    pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
+        sqlx::query_as("SELECT [id], [email] FROM [User] WHERE [id] = $1")
+            .bind(user_id)
+            .fetch_optional(&self.pool)
+            .await
+            .map_err(DBError::from)
+    }
+
+    pub async fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResult> {
         self.sign_up_with_given_time(email, password, Utc::now())
+            .await
     }
 
-    fn sign_up_with_given_time(
+    async fn sign_up_with_given_time(
         &self,
         email: &str,
         password: &str,
         datetime: DateTime<Utc>,
     ) -> Result<SignUpResult> {
-        let mut con = self.get()?;
-        let tx = con.transaction()?;
-        let token = match tx
-            .query_row(
-                "SELECT [id], [validation_token] FROM [User] WHERE [email] = ?1",
-                [email],
-                |r| {
-                    Ok((
-                        r.get::<&str, i64>("id")?,
-                        r.get::<&str, Option<String>>("validation_token")?,
-                    ))
-                },
-            )
-            .optional()?
+        let mut tx = self.tx().await?;
+
+        let token = match sqlx::query_as::<_, (i64, Option<String>)>(
+            r#"
+SELECT [id], [validation_token]
+FROM [User] WHERE [email] = $1
+            "#,
+        )
+        .bind(email)
+        .fetch_optional(&mut *tx)
+        .await?
         {
             Some((id, validation_token)) => {
                 if validation_token.is_none() {
@@ -293,91 +266,95 @@ impl Connection {
                 }
                 let token = generate_token();
                 let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
-                tx.execute(
-                    "UPDATE [User]
-                    SET [validation_token] = ?2, [creation_datetime] = ?3, [password] = ?4
-                    WHERE [id] = ?1",
-                    params![id, token, datetime, hashed_password],
-                )?;
+                sqlx::query(
+                    r#"
+UPDATE [User]
+SET [validation_token] = $2, [creation_datetime] = $3, [password] = $4
+WHERE [id] = $1
+                    "#,
+                )
+                .bind(id)
+                .bind(&token)
+                .bind(datetime)
+                .bind(hashed_password)
+                .execute(&mut *tx)
+                .await?;
                 token
             }
             None => {
                 let token = generate_token();
                 let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
-                tx.execute(
-                    "INSERT INTO [User]
-                    ([email], [validation_token], [creation_datetime], [password])
-                    VALUES (?1, ?2, ?3, ?4)",
-                    params![email, token, datetime, hashed_password],
-                )?;
+                sqlx::query(
+                    r#"
+INSERT INTO [User]
+([email], [validation_token], [creation_datetime], [password])
+VALUES ($1, $2, $3, $4)
+                    "#,
+                )
+                .bind(email)
+                .bind(&token)
+                .bind(datetime)
+                .bind(hashed_password)
+                .execute(&mut *tx)
+                .await?;
                 token
             }
         };
-        tx.commit()?;
+
+        tx.commit().await?;
+
         Ok(SignUpResult::UserCreatedWaitingForValidation(token))
     }
 
-    pub fn validation(
+    pub async fn validation(
         &self,
         token: &str,
         validation_time: Duration,
         ip: &str,
         user_agent: &str,
     ) -> Result<ValidationResult> {
-        let mut con = self.get()?;
-        let tx = con.transaction()?;
-        let user_id = match tx
-            .query_row(
-                "SELECT [id], [creation_datetime] FROM [User] WHERE [validation_token] = ?1",
-                [token],
-                |r| {
-                    Ok((
-                        r.get::<&str, i64>("id")?,
-                        r.get::<&str, DateTime<Utc>>("creation_datetime")?,
-                    ))
-                },
-            )
-            .optional()?
+        let mut tx = self.tx().await?;
+
+        let user_id = match sqlx::query_as::<_, (i64, DateTime<Utc>)>(
+            "SELECT [id], [creation_datetime] FROM [User] WHERE [validation_token] = $1",
+        )
+        .bind(token)
+        .fetch_optional(&mut *tx)
+        .await?
         {
             Some((id, creation_datetime)) => {
                 if Utc::now() - creation_datetime > validation_time {
                     return Ok(ValidationResult::ValidationExpired);
                 }
-                tx.execute(
-                    "UPDATE [User] SET [validation_token] = NULL WHERE [id] = ?1",
-                    [id],
-                )?;
+                sqlx::query("UPDATE [User] SET [validation_token] = NULL WHERE [id] = $1")
+                    .bind(id)
+                    .execute(&mut *tx)
+                    .await?;
                 id
             }
             None => return Ok(ValidationResult::UnknownUser),
         };
-        let token = Connection::create_login_token(&tx, user_id, ip, user_agent)?;
-        tx.commit()?;
+
+        let token = Self::create_login_token(&mut tx, user_id, ip, user_agent).await?;
+
+        tx.commit().await?;
         Ok(ValidationResult::Ok(token, user_id))
     }
 
-    pub fn sign_in(
+    pub async fn sign_in(
         &self,
         email: &str,
         password: &str,
         ip: &str,
         user_agent: &str,
     ) -> Result<SignInResult> {
-        let mut con = self.get()?;
-        let tx = con.transaction()?;
-        match tx
-            .query_row(
-                "SELECT [id], [password], [validation_token] FROM [User] WHERE [email] = ?1",
-                [email],
-                |r| {
-                    Ok((
-                        r.get::<&str, i64>("id")?,
-                        r.get::<&str, String>("password")?,
-                        r.get::<&str, Option<String>>("validation_token")?,
-                    ))
-                },
-            )
-            .optional()?
+        let mut tx = self.tx().await?;
+        match sqlx::query_as::<_, (i64, String, Option<String>)>(
+            "SELECT [id], [password], [validation_token] FROM [User] WHERE [email] = $1",
+        )
+        .bind(email)
+        .fetch_optional(&mut *tx)
+        .await?
         {
             Some((id, stored_password, validation_token)) => {
                 if validation_token.is_some() {
@@ -385,8 +362,8 @@ impl Connection {
                 } else if verify_password(password, &stored_password)
                     .map_err(DBError::from_dyn_error)?
                 {
-                    let token = Connection::create_login_token(&tx, id, ip, user_agent)?;
-                    tx.commit()?;
+                    let token = Self::create_login_token(&mut tx, id, ip, user_agent).await?;
+                    tx.commit().await?;
                     Ok(SignInResult::Ok(token, id))
                 } else {
                     Ok(SignInResult::WrongPassword)
@@ -396,138 +373,157 @@ impl Connection {
         }
     }
 
-    pub fn authentication(
+    pub async fn authentication(
         &self,
         token: &str,
         ip: &str,
         user_agent: &str,
     ) -> Result<AuthenticationResult> {
-        let mut con = self.get()?;
-        let tx = con.transaction()?;
-        match tx
-            .query_row(
-                "SELECT [id], [user_id] FROM [UserLoginToken] WHERE [token] = ?1",
-                [token],
-                |r| Ok((r.get::<&str, i64>("id")?, r.get::<&str, i64>("user_id")?)),
-            )
-            .optional()?
+        let mut tx = self.tx().await?;
+        match sqlx::query_as::<_, (i64, i64)>(
+            "SELECT [id], [user_id] FROM [UserLoginToken] WHERE [token] = $1",
+        )
+        .bind(token)
+        .fetch_optional(&mut *tx)
+        .await?
         {
             Some((login_id, user_id)) => {
-                tx.execute(
-                    "UPDATE [UserLoginToken]
-                    SET [last_login_datetime] = ?2, [ip] = ?3, [user_agent] = ?4
-                    WHERE [id] = ?1",
-                    params![login_id, Utc::now(), ip, user_agent],
-                )?;
-                tx.commit()?;
+                sqlx::query(
+                    r#"
+UPDATE [UserLoginToken]
+SET [last_login_datetime] = $2, [ip] = $3, [user_agent] = $4
+WHERE [id] = $1
+                    "#,
+                )
+                .bind(login_id)
+                .bind(Utc::now())
+                .bind(ip)
+                .bind(user_agent)
+                .execute(&mut *tx)
+                .await?;
+                tx.commit().await?;
                 Ok(AuthenticationResult::Ok(user_id))
             }
             None => Ok(AuthenticationResult::NotValidToken),
         }
     }
 
-    pub fn sign_out(&self, token: &str) -> Result<()> {
-        let mut con = self.get()?;
-        let tx = con.transaction()?;
-        match tx
-            .query_row(
-                "SELECT [id] FROM [UserLoginToken] WHERE [token] = ?1",
-                [token],
-                |r| Ok(r.get::<&str, i64>("id")?),
-            )
-            .optional()?
+    pub async fn sign_out(&self, token: &str) -> Result<()> {
+        let mut tx = self.tx().await?;
+        match sqlx::query_scalar::<_, i64>("SELECT [id] FROM [UserLoginToken] WHERE [token] = $1")
+            .bind(token)
+            .fetch_optional(&mut *tx)
+            .await?
         {
             Some(login_id) => {
-                tx.execute(
-                    "DELETE FROM [UserLoginToken] WHERE [id] = ?1",
-                    params![login_id],
-                )?;
-                tx.commit()?
+                sqlx::query("DELETE FROM [UserLoginToken] WHERE [id] = $1")
+                    .bind(login_id)
+                    .execute(&mut *tx)
+                    .await?;
+                tx.commit().await?;
             }
             None => (),
         }
         Ok(())
     }
 
-    pub fn create_recipe(&self, user_id: i64) -> Result<i64> {
-        let con = self.get()?;
-
-        // Verify if an empty recipe already exists. Returns its id if one exists.
-        match con
-            .query_row(
-                "SELECT [Recipe].[id] FROM [Recipe]
-                LEFT JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id]
-                LEFT JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
-                WHERE [Recipe].[user_id] = ?1
-                    AND [Recipe].[title] = ''
-                    AND [Recipe].[estimate_time] IS NULL
-                    AND [Recipe].[description] = ''
-                    AND [Image].[id] IS NULL
-                    AND [Group].[id] IS NULL",
-                [user_id],
-                |r| Ok(r.get::<&str, i64>("id")?),
-            )
-            .optional()?
+    pub async fn create_recipe(&self, user_id: i64) -> Result<i64> {
+        let mut tx = self.tx().await?;
+
+        match sqlx::query_scalar::<_, i64>(
+            r#"
+SELECT [Recipe].[id] FROM [Recipe]
+LEFT JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id]
+LEFT JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
+WHERE [Recipe].[user_id] = $1
+    AND [Recipe].[title] = ''
+    AND [Recipe].[estimate_time] IS NULL
+    AND [Recipe].[description] = ''
+    AND [Image].[id] IS NULL
+    AND [Group].[id] IS NULL
+            "#,
+        )
+        .bind(user_id)
+        .fetch_optional(&mut *tx)
+        .await?
         {
             Some(recipe_id) => Ok(recipe_id),
             None => {
-                con.execute(
-                    "INSERT INTO [Recipe] ([user_id], [title]) VALUES (?1, '')",
-                    [user_id],
-                )?;
-                Ok(con.last_insert_rowid())
+                let db_result =
+                    sqlx::query("INSERT INTO [Recipe] ([user_id], [title]) VALUES ($1, '')")
+                        .bind(user_id)
+                        .execute(&mut *tx)
+                        .await?;
+
+                tx.commit().await?;
+                Ok(db_result.last_insert_rowid())
             }
         }
     }
 
-    pub fn set_recipe_title(&self, recipe_id: i64, title: &str) -> Result<()> {
-        let con = self.get()?;
-        con.execute(
-            "UPDATE [Recipe] SET [title] = ?2 WHERE [id] = ?1",
-            params![recipe_id, title],
-        )
-        .map(|_n| ())
-        .map_err(DBError::from)
+    pub async fn set_recipe_title(&self, recipe_id: i64, title: &str) -> Result<()> {
+        sqlx::query("UPDATE [Recipe] SET [title] = $2 WHERE [id] = $1")
+            .bind(recipe_id)
+            .bind(title)
+            .execute(&self.pool)
+            .await
+            .map(|_| ())
+            .map_err(DBError::from)
     }
 
-    pub fn set_recipe_description(&self, recipe_id: i64, description: &str) -> Result<()> {
-        let con = self.get()?;
-        con.execute(
-            "UPDATE [Recipe] SET [description] = ?2 WHERE [id] = ?1",
-            params![recipe_id, description],
-        )
-        .map(|_n| ())
-        .map_err(DBError::from)
+    pub async fn set_recipe_description(&self, recipe_id: i64, description: &str) -> Result<()> {
+        sqlx::query("UPDATE [Recipe] SET [description] = $2 WHERE [id] = $1")
+            .bind(recipe_id)
+            .bind(description)
+            .execute(&self.pool)
+            .await
+            .map(|_| ())
+            .map_err(DBError::from)
     }
 
     /// Execute a given SQL file.
-    pub fn execute_file<P: AsRef<Path> + fmt::Display>(&self, file: P) -> Result<()> {
-        let con = self.get()?;
+    pub async fn execute_file<P: AsRef<Path> + fmt::Display>(&self, file: P) -> Result<()> {
         let sql = load_sql_file(file)?;
-        con.execute_batch(&sql).map_err(DBError::from)
+        sqlx::query(&sql)
+            .execute(&self.pool)
+            .await
+            .map(|_| ())
+            .map_err(DBError::from)
     }
 
-    /// Execute any SQL statement.
-    /// Mainly used for testing.
-    pub fn execute_sql<P: Params>(&self, sql: &str, params: P) -> Result<usize> {
-        let con = self.get()?;
-        con.execute(sql, params).map_err(DBError::from)
+    pub async fn execute_sql<'a>(
+        &self,
+        query: sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>>,
+    ) -> Result<u64> {
+        query
+            .execute(&self.pool)
+            .await
+            .map(|db_result| db_result.rows_affected())
+            .map_err(DBError::from)
     }
 
     // Return the token.
-    fn create_login_token(
-        tx: &rusqlite::Transaction,
+    async fn create_login_token(
+        tx: &mut sqlx::Transaction<'_, Sqlite>,
         user_id: i64,
         ip: &str,
         user_agent: &str,
     ) -> Result<String> {
         let token = generate_token();
-        tx.execute(
-            "INSERT INTO [UserLoginToken]
-            ([user_id], [last_login_datetime], [token], [ip], [user_agent])
-            VALUES (?1, ?2, ?3, ?4, ?5)",
-            params![user_id, Utc::now(), token, ip, user_agent],
-        )?;
+        sqlx::query(
+            r#"
+INSERT INTO [UserLoginToken]
+([user_id], [last_login_datetime], [token], [ip], [user_agent])
+VALUES ($1, $2, $3, $4, $5)
+            "#,
+        )
+        .bind(user_id)
+        .bind(Utc::now())
+        .bind(&token)
+        .bind(ip)
+        .bind(user_agent)
+        .execute(&mut **tx)
+        .await?;
         Ok(token)
     }
 }
@@ -558,52 +554,57 @@ fn generate_token() -> String {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use rusqlite::{ffi, types::Value, Error, ErrorCode};
 
-    #[test]
-    fn sign_up() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
-        match connection.sign_up("paul@atreides.com", "12345")? {
+    #[tokio::test]
+    async fn sign_up() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
+        match connection.sign_up("paul@atreides.com", "12345").await? {
             SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
             other => panic!("{:?}", other),
         }
         Ok(())
     }
 
-    #[test]
-    fn sign_up_to_an_already_existing_user() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
-        connection.execute_sql("
-            INSERT INTO
-                [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
-                VALUES (
-                    1,
-                    'paul@atreides.com',
-                    'paul',
-                    '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
-                    0,
-                    NULL
-                );", [])?;
-        match connection.sign_up("paul@atreides.com", "12345")? {
+    #[tokio::test]
+    async fn sign_up_to_an_already_existing_user() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
+        connection.execute_sql(
+            sqlx::query(
+            r#"
+INSERT INTO
+    [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
+    VALUES (
+        1,
+        'paul@atreides.com',
+        'paul',
+        '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
+        0,
+        NULL
+    );
+            "#)).await?;
+        match connection.sign_up("paul@atreides.com", "12345").await? {
             SignUpResult::UserAlreadyExists => (), // Nominal case.
             other => panic!("{:?}", other),
         }
         Ok(())
     }
 
-    #[test]
-    fn sign_up_and_sign_in_without_validation() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
+    #[tokio::test]
+    async fn sign_up_and_sign_in_without_validation() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
 
         let email = "paul@atreides.com";
         let password = "12345";
 
-        match connection.sign_up(email, password)? {
+        match connection.sign_up(email, password).await? {
             SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
             other => panic!("{:?}", other),
         }
 
-        match connection.sign_in(email, password, "127.0.0.1", "Mozilla/5.0")? {
+        match connection
+            .sign_in(email, password, "127.0.0.1", "Mozilla/5.0")
+            .await?
+        {
             SignInResult::AccountNotValidated => (), // Nominal case.
             other => panic!("{:?}", other),
         }
@@ -611,116 +612,134 @@ mod tests {
         Ok(())
     }
 
-    #[test]
-    fn sign_up_to_an_unvalidated_already_existing_user() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
+    #[tokio::test]
+    async fn sign_up_to_an_unvalidated_already_existing_user() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
         let token = generate_token();
-        connection.execute_sql("
-            INSERT INTO
-                [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
-                VALUES (
-                    1,
-                    'paul@atreides.com',
-                    'paul',
-                    '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
-                    0,
-                    :token
-                );", named_params! { ":token": token })?;
-        match connection.sign_up("paul@atreides.com", "12345")? {
+        connection.execute_sql(
+            sqlx::query(
+            r#"
+INSERT INTO [User]
+    ([id], [email], [name], [password], [creation_datetime], [validation_token])
+VALUES (
+    1,
+    'paul@atreides.com',
+    'paul',
+    '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
+    0,
+    $1
+)
+            "#
+        ).bind(token)).await?;
+        match connection.sign_up("paul@atreides.com", "12345").await? {
             SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
             other => panic!("{:?}", other),
         }
         Ok(())
     }
 
-    #[test]
-    fn sign_up_then_send_validation_at_time() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
-        let validation_token = match connection.sign_up("paul@atreides.com", "12345")? {
+    #[tokio::test]
+    async fn sign_up_then_send_validation_at_time() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
+        let validation_token = match connection.sign_up("paul@atreides.com", "12345").await? {
             SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
             other => panic!("{:?}", other),
         };
-        match connection.validation(
-            &validation_token,
-            Duration::hours(1),
-            "127.0.0.1",
-            "Mozilla/5.0",
-        )? {
+        match connection
+            .validation(
+                &validation_token,
+                Duration::hours(1),
+                "127.0.0.1",
+                "Mozilla/5.0",
+            )
+            .await?
+        {
             ValidationResult::Ok(_, _) => (), // Nominal case.
             other => panic!("{:?}", other),
         }
         Ok(())
     }
 
-    #[test]
-    fn sign_up_then_send_validation_too_late() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
-        let validation_token = match connection.sign_up_with_given_time(
-            "paul@atreides.com",
-            "12345",
-            Utc::now() - Duration::days(1),
-        )? {
+    #[tokio::test]
+    async fn sign_up_then_send_validation_too_late() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
+        let validation_token = match connection
+            .sign_up_with_given_time("paul@atreides.com", "12345", Utc::now() - Duration::days(1))
+            .await?
+        {
             SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
             other => panic!("{:?}", other),
         };
-        match connection.validation(
-            &validation_token,
-            Duration::hours(1),
-            "127.0.0.1",
-            "Mozilla/5.0",
-        )? {
+        match connection
+            .validation(
+                &validation_token,
+                Duration::hours(1),
+                "127.0.0.1",
+                "Mozilla/5.0",
+            )
+            .await?
+        {
             ValidationResult::ValidationExpired => (), // Nominal case.
             other => panic!("{:?}", other),
         }
         Ok(())
     }
 
-    #[test]
-    fn sign_up_then_send_validation_with_bad_token() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
-        let _validation_token = match connection.sign_up("paul@atreides.com", "12345")? {
+    #[tokio::test]
+    async fn sign_up_then_send_validation_with_bad_token() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
+        let _validation_token = match connection.sign_up("paul@atreides.com", "12345").await? {
             SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
             other => panic!("{:?}", other),
         };
         let random_token = generate_token();
-        match connection.validation(
-            &random_token,
-            Duration::hours(1),
-            "127.0.0.1",
-            "Mozilla/5.0",
-        )? {
+        match connection
+            .validation(
+                &random_token,
+                Duration::hours(1),
+                "127.0.0.1",
+                "Mozilla/5.0",
+            )
+            .await?
+        {
             ValidationResult::UnknownUser => (), // Nominal case.
             other => panic!("{:?}", other),
         }
         Ok(())
     }
 
-    #[test]
-    fn sign_up_then_send_validation_then_sign_in() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
+    #[tokio::test]
+    async fn sign_up_then_send_validation_then_sign_in() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
 
         let email = "paul@atreides.com";
         let password = "12345";
 
         // Sign up.
-        let validation_token = match connection.sign_up(email, password)? {
+        let validation_token = match connection.sign_up(email, password).await? {
             SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
             other => panic!("{:?}", other),
         };
 
         // Validation.
-        match connection.validation(
-            &validation_token,
-            Duration::hours(1),
-            "127.0.0.1",
-            "Mozilla/5.0",
-        )? {
+        match connection
+            .validation(
+                &validation_token,
+                Duration::hours(1),
+                "127.0.0.1",
+                "Mozilla/5.0",
+            )
+            .await?
+        {
             ValidationResult::Ok(_, _) => (),
             other => panic!("{:?}", other),
         };
 
         // Sign in.
-        match connection.sign_in(email, password, "127.0.0.1", "Mozilla/5.0")? {
+        match connection
+            .sign_in(email, password, "127.0.0.1", "Mozilla/5.0")
+            .await?
+        {
             SignInResult::Ok(_, _) => (), // Nominal case.
             other => panic!("{:?}", other),
         }
@@ -728,94 +747,113 @@ mod tests {
         Ok(())
     }
 
-    #[test]
-    fn sign_up_then_send_validation_then_authentication() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
+    #[tokio::test]
+    async fn sign_up_then_send_validation_then_authentication() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
 
         let email = "paul@atreides.com";
         let password = "12345";
 
         // Sign up.
-        let validation_token = match connection.sign_up(email, password)? {
+        let validation_token = match connection.sign_up(email, password).await? {
             SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
             other => panic!("{:?}", other),
         };
 
         // Validation.
-        let (authentication_token, user_id) = match connection.validation(
-            &validation_token,
-            Duration::hours(1),
-            "127.0.0.1",
-            "Mozilla",
-        )? {
+        let (authentication_token, user_id) = match connection
+            .validation(
+                &validation_token,
+                Duration::hours(1),
+                "127.0.0.1",
+                "Mozilla",
+            )
+            .await?
+        {
             ValidationResult::Ok(token, user_id) => (token, user_id),
             other => panic!("{:?}", other),
         };
 
         // Check user login information.
-        let user_login_info_1 = connection.get_user_login_info(&authentication_token)?;
+        let user_login_info_1 = connection
+            .get_user_login_info(&authentication_token)
+            .await?;
         assert_eq!(user_login_info_1.ip, "127.0.0.1");
         assert_eq!(user_login_info_1.user_agent, "Mozilla");
 
         // Authentication.
-        let _user_id =
-            match connection.authentication(&authentication_token, "192.168.1.1", "Chrome")? {
-                AuthenticationResult::Ok(user_id) => user_id, // Nominal case.
-                other => panic!("{:?}", other),
-            };
+        let _user_id = match connection
+            .authentication(&authentication_token, "192.168.1.1", "Chrome")
+            .await?
+        {
+            AuthenticationResult::Ok(user_id) => user_id, // Nominal case.
+            other => panic!("{:?}", other),
+        };
 
         // Check user login information.
-        let user_login_info_2 = connection.get_user_login_info(&authentication_token)?;
+        let user_login_info_2 = connection
+            .get_user_login_info(&authentication_token)
+            .await?;
+
         assert_eq!(user_login_info_2.ip, "192.168.1.1");
         assert_eq!(user_login_info_2.user_agent, "Chrome");
 
         Ok(())
     }
 
-    #[test]
-    fn sign_up_then_send_validation_then_sign_out_then_sign_in() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
+    #[tokio::test]
+    async fn sign_up_then_send_validation_then_sign_out_then_sign_in() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
 
         let email = "paul@atreides.com";
         let password = "12345";
 
         // Sign up.
-        let validation_token = match connection.sign_up(email, password)? {
+        let validation_token = match connection.sign_up(email, password).await? {
             SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
             other => panic!("{:?}", other),
         };
 
         // Validation.
-        let (authentication_token_1, user_id_1) = match connection.validation(
-            &validation_token,
-            Duration::hours(1),
-            "127.0.0.1",
-            "Mozilla",
-        )? {
+        let (authentication_token_1, user_id_1) = match connection
+            .validation(
+                &validation_token,
+                Duration::hours(1),
+                "127.0.0.1",
+                "Mozilla",
+            )
+            .await?
+        {
             ValidationResult::Ok(token, user_id) => (token, user_id),
             other => panic!("{:?}", other),
         };
 
         // Check user login information.
-        let user_login_info_1 = connection.get_user_login_info(&authentication_token_1)?;
+        let user_login_info_1 = connection
+            .get_user_login_info(&authentication_token_1)
+            .await?;
         assert_eq!(user_login_info_1.ip, "127.0.0.1");
         assert_eq!(user_login_info_1.user_agent, "Mozilla");
 
         // Sign out.
-        connection.sign_out(&authentication_token_1)?;
+        connection.sign_out(&authentication_token_1).await?;
 
         // Sign in.
-        let (authentication_token_2, user_id_2) =
-            match connection.sign_in(email, password, "192.168.1.1", "Chrome")? {
-                SignInResult::Ok(token, user_id) => (token, user_id),
-                other => panic!("{:?}", other),
-            };
+        let (authentication_token_2, user_id_2) = match connection
+            .sign_in(email, password, "192.168.1.1", "Chrome")
+            .await?
+        {
+            SignInResult::Ok(token, user_id) => (token, user_id),
+            other => panic!("{:?}", other),
+        };
 
         assert_eq!(user_id_1, user_id_2);
         assert_ne!(authentication_token_1, authentication_token_2);
 
         // Check user login information.
-        let user_login_info_2 = connection.get_user_login_info(&authentication_token_2)?;
+        let user_login_info_2 = connection
+            .get_user_login_info(&authentication_token_2)
+            .await?;
 
         assert_eq!(user_login_info_2.ip, "192.168.1.1");
         assert_eq!(user_login_info_2.user_agent, "Chrome");
@@ -823,44 +861,44 @@ mod tests {
         Ok(())
     }
 
-    #[test]
-    fn create_a_new_recipe_then_update_its_title() -> Result<()> {
-        let connection = Connection::new_in_memory()?;
+    #[tokio::test]
+    async fn create_a_new_recipe_then_update_its_title() -> Result<()> {
+        let connection = Connection::new_in_memory().await?;
 
         connection.execute_sql(
-            "INSERT INTO [User]
-            ([id], [email], [name], [password], [creation_datetime], [validation_token])
-            VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
-            params![
-                1,
-                "paul@atreides.com",
-                "paul",
-                "$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc",
-                "2022-11-29 22:05:04.121407300+00:00",
-                Value::Null,
-            ]
-        )?;
-
-        match connection.create_recipe(2) {
-            Err(DBError::SqliteError(Error::SqliteFailure(
-                ffi::Error {
-                    code: ErrorCode::ConstraintViolation,
-                    extended_code: _,
-                },
-                Some(_),
-            ))) => (), // Nominal case.
+            sqlx::query(
+            r#"
+INSERT INTO [User]
+    ([id], [email], [name], [password], [creation_datetime], [validation_token])
+VALUES
+    ($1, $2, $3, $4, $5, $6)
+            "#
+            )
+            .bind(1)
+            .bind("paul@atreides.com")
+            .bind("paul")
+            .bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
+            .bind("2022-11-29 22:05:04.121407300+00:00")
+            .bind(None::<&str>) // 'null'.
+        ).await?;
+
+        match connection.create_recipe(2).await {
+            Err(DBError::Sqlx(sqlx::Error::Database(err))) => {
+                // SQLITE_CONSTRAINT_FOREIGNKEY
+                // https://www.sqlite.org/rescode.html#constraint_foreignkey
+                assert_eq!(err.code(), Some(std::borrow::Cow::from("787")));
+            } // Nominal case. TODO: check 'err' value.
             other => panic!(
                 "Creating a recipe with an inexistant user must fail: {:?}",
                 other
             ),
         }
 
-        let recipe_id = connection.create_recipe(1)?;
+        let recipe_id = connection.create_recipe(1).await?;
         assert_eq!(recipe_id, 1);
 
-        connection.set_recipe_title(recipe_id, "Crêpe")?;
-
-        let recipe = connection.get_recipe(recipe_id)?;
+        connection.set_recipe_title(recipe_id, "Crêpe").await?;
+        let recipe = connection.get_recipe(recipe_id).await?.unwrap();
         assert_eq!(recipe.title, "Crêpe".to_string());
 
         Ok(())
index edc9cb9..98b6ecd 100644 (file)
@@ -1,2 +1,2 @@
-pub mod asynchronous;
 pub mod db;
+mod utils;
diff --git a/backend/src/data/utils.rs b/backend/src/data/utils.rs
new file mode 100644 (file)
index 0000000..c32cfa2
--- /dev/null
@@ -0,0 +1,33 @@
+use sqlx::{sqlite::SqliteRow, FromRow, Row};
+
+use crate::model;
+
+impl FromRow<'_, SqliteRow> for model::Recipe {
+    fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
+        Ok(model::Recipe::new(
+            row.try_get("id")?,
+            row.try_get("user_id")?,
+            row.try_get("title")?,
+            row.try_get("description")?,
+        ))
+    }
+}
+
+impl FromRow<'_, SqliteRow> for model::UserLoginInfo {
+    fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
+        Ok(model::UserLoginInfo {
+            last_login_datetime: row.try_get("last_login_datetime")?,
+            ip: row.try_get("ip")?,
+            user_agent: row.try_get("user_agent")?,
+        })
+    }
+}
+
+impl FromRow<'_, SqliteRow> for model::User {
+    fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
+        Ok(model::User {
+            id: row.try_get("id")?,
+            email: row.try_get("email")?,
+        })
+    }
+}
index 4d906ff..a167e86 100644 (file)
@@ -1,6 +1,8 @@
 use derive_more::Display;
-use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
-use std::time::Duration;
+use lettre::{
+    transport::smtp::{authentication::Credentials, AsyncSmtpTransport},
+    AsyncTransport, Message, Tokio1Executor,
+};
 
 use crate::consts;
 
@@ -29,10 +31,11 @@ impl From<lettre::error::Error> for Error {
     }
 }
 
-pub fn send_validation(
+pub async fn send_validation(
     site_url: &str,
     email: &str,
     token: &str,
+    smtp_relay_address: &str,
     smtp_login: &str,
     smtp_password: &str,
 ) -> Result<(), Error> {
@@ -40,20 +43,20 @@ pub fn send_validation(
         .message_id(None)
         .from("recipes@gburri.org".parse()?)
         .to(email.parse()?)
-        .subject("Recipes.gburri.org account validation")
+        .subject("recipes.gburri.org account validation")
         .body(format!(
-            "Follow this link to confirm your inscription: {}/validation?token={}",
+            "Follow this link to confirm your inscription: {}/validation?validation_token={}",
             site_url, token
         ))?;
 
     let credentials = Credentials::new(smtp_login.to_string(), smtp_password.to_string());
 
-    let mailer = SmtpTransport::relay("mail.gandi.net")?
+    let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(smtp_relay_address)?
         .credentials(credentials)
         .timeout(Some(consts::SEND_EMAIL_TIMEOUT))
         .build();
 
-    if let Err(error) = mailer.send(&email) {
+    if let Err(error) = mailer.send(email).await {
         eprintln!("Error when sending E-mail:\n{:?}", &error);
     }
 
index 97eaa17..65a778b 100644 (file)
@@ -1,9 +1,17 @@
-use std::path::Path;
-
-use actix_files as fs;
-use actix_web::{middleware, web, App, HttpServer};
+use std::{net::SocketAddr, path::Path};
+
+use axum::{
+    extract::{ConnectInfo, FromRef, Request, State},
+    middleware::{self, Next},
+    response::{Response, Result},
+    routing::get,
+    Router,
+};
+use axum_extra::extract::cookie::CookieJar;
 use chrono::prelude::*;
 use clap::Parser;
+use config::Config;
+use tower_http::services::ServeDir;
 
 use data::db;
 
@@ -16,49 +24,121 @@ mod model;
 mod services;
 mod utils;
 
-#[actix_web::main]
-async fn main() -> std::io::Result<()> {
-    if process_args() {
-        return Ok(());
+#[derive(Clone)]
+struct AppState {
+    config: Config,
+    db_connection: db::Connection,
+}
+
+impl FromRef<AppState> for Config {
+    fn from_ref(app_state: &AppState) -> Config {
+        app_state.config.clone()
+    }
+}
+
+impl FromRef<AppState> for db::Connection {
+    fn from_ref(app_state: &AppState) -> db::Connection {
+        app_state.db_connection.clone()
     }
+}
 
-    std::env::set_var("RUST_LOG", "info,actix_web=info");
-    env_logger::init();
+// TODO: Should main returns 'Result'?
+#[tokio::main]
+async fn main() {
+    if process_args().await {
+        return;
+    }
 
     println!("Starting Recipes as web server...");
 
-    let config = web::Data::new(config::load());
-    let port = config.as_ref().port;
+    let config = config::load();
+    let port = config.port;
 
     println!("Configuration: {:?}", config);
 
-    let db_connection = web::Data::new(db::Connection::new().unwrap());
-
-    let server = HttpServer::new(move || {
-        App::new()
-            .wrap(middleware::Logger::default())
-            .wrap(middleware::Compress::default())
-            .app_data(db_connection.clone())
-            .app_data(config.clone())
-            .service(services::home_page)
-            .service(services::sign_up_get)
-            .service(services::sign_up_post)
-            .service(services::sign_up_check_email)
-            .service(services::sign_up_validation)
-            .service(services::sign_in_get)
-            .service(services::sign_in_post)
-            .service(services::sign_out)
-            .service(services::view_recipe)
-            .service(services::edit_recipe)
-            .service(services::new_recipe)
-            .service(services::webapi::set_recipe_title)
-            .service(services::webapi::set_recipe_description)
-            .service(fs::Files::new("/static", "static"))
-            .default_service(web::to(services::not_found))
-    });
-    //.workers(1);
-
-    server.bind(&format!("0.0.0.0:{}", port))?.run().await
+    tracing_subscriber::fmt::init();
+
+    let db_connection = db::Connection::new().await.unwrap();
+
+    let state = AppState {
+        config,
+        db_connection,
+    };
+
+    let app = Router::new()
+        .route("/", get(services::home_page))
+        .route(
+            "/signup",
+            get(services::sign_up_get).post(services::sign_up_post),
+        )
+        .route("/validation", get(services::sign_up_validation))
+        .route("/recipe/view/:id", get(services::view_recipe))
+        .route(
+            "/signin",
+            get(services::sign_in_get).post(services::sign_in_post),
+        )
+        .route("/signout", get(services::sign_out))
+        .route_layer(middleware::from_fn_with_state(
+            state.clone(),
+            user_authentication,
+        ))
+        .nest_service("/static", ServeDir::new("static"))
+        .fallback(services::not_found)
+        .with_state(state)
+        .into_make_service_with_connect_info::<SocketAddr>();
+
+    let addr = SocketAddr::from(([0, 0, 0, 0], port));
+    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+
+    axum::serve(listener, app).await.unwrap();
+}
+
+async fn user_authentication(
+    ConnectInfo(addr): ConnectInfo<SocketAddr>,
+    State(connection): State<db::Connection>,
+    mut req: Request,
+    next: Next,
+) -> Result<Response> {
+    let jar = CookieJar::from_headers(req.headers());
+    let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(req.headers(), addr);
+    let user = get_current_user(connection, &jar, &client_ip, &client_user_agent).await;
+    req.extensions_mut().insert(user);
+    Ok(next.run(req).await)
+}
+
+async fn get_current_user(
+    connection: db::Connection,
+    jar: &CookieJar,
+    client_ip: &str,
+    client_user_agent: &str,
+) -> Option<model::User> {
+    match jar.get(consts::COOKIE_AUTH_TOKEN_NAME) {
+        Some(token_cookie) => match connection
+            .authentication(token_cookie.value(), &client_ip, &client_user_agent)
+            .await
+        {
+            Ok(db::AuthenticationResult::NotValidToken) => {
+                // TODO: remove cookie?
+                None
+            }
+            Ok(db::AuthenticationResult::Ok(user_id)) => {
+                match connection.load_user(user_id).await {
+                    Ok(user) => user,
+                    Err(error) => {
+                        // TODO: Return 'Result'?
+                        println!("Error during authentication: {}", error);
+                        None
+                    }
+                }
+            }
+            Err(error) => {
+                // TODO: Return 'Result'?
+                println!("Error during authentication: {}", error);
+                None
+            }
+        },
+        None => None,
+    }
 }
 
 #[derive(Parser, Debug)]
@@ -68,7 +148,7 @@ struct Args {
     dbtest: bool,
 }
 
-fn process_args() -> bool {
+async fn process_args() -> bool {
     let args = Args::parse();
 
     if args.dbtest {
@@ -93,16 +173,18 @@ fn process_args() -> bool {
                 .expect(&format!("Unable to remove db file: {:?}", &db_path));
         }
 
-        match db::Connection::new() {
+        match db::Connection::new().await {
             Ok(con) => {
-                if let Err(error) = con.execute_file("sql/data_test.sql") {
+                if let Err(error) = con.execute_file("sql/data_test.sql").await {
                     eprintln!("{}", error);
                 }
                 // Set the creation datetime to 'now'.
                 con.execute_sql(
-                    "UPDATE [User] SET [creation_datetime] = ?1 WHERE [email] = 'paul@test.org'",
-                    [Utc::now()],
+                    sqlx::query(
+                    "UPDATE [User] SET [creation_datetime] = ?1 WHERE [email] = 'paul@test.org'")
+                    .bind(Utc::now())
                 )
+                .await
                 .unwrap();
             }
             Err(error) => {
index 0ed4825..011cab2 100644 (file)
@@ -1,5 +1,6 @@
 use chrono::prelude::*;\r
 \r
+#[derive(Debug, Clone)]\r
 pub struct User {\r
     pub id: i64,\r
     pub email: String,\r
index 63a7461..a8f440d 100644 (file)
-use std::collections::HashMap;
-
-use actix_web::{
-    cookie::Cookie,
-    get,
-    http::{header, header::ContentType, StatusCode},
-    post, web, HttpRequest, HttpResponse, Responder,
+use std::{collections::HashMap, net::SocketAddr};
+
+use askama::Template;
+use axum::{
+    body::Body,
+    debug_handler,
+    extract::{ConnectInfo, Extension, Host, Path, Query, Request, State},
+    http::{HeaderMap, StatusCode},
+    response::{IntoResponse, Redirect, Response, Result},
+    Form,
 };
-use askama_actix::{Template, TemplateToResponse};
+use axum_extra::extract::cookie::{Cookie, CookieJar};
 use chrono::Duration;
-use log::{debug, error, info, log_enabled, Level};
 use serde::Deserialize;
 
-use crate::{
-    config::Config,
-    consts,
-    data::{asynchronous, db},
-    email, model, utils,
-};
+use crate::{config::Config, consts, data::db, email, model, utils, AppState};
 
 pub mod webapi;
 
-///// UTILS /////
-
-fn get_ip_and_user_agent(req: &HttpRequest) -> (String, String) {
-    let ip = match req.headers().get(consts::REVERSE_PROXY_IP_HTTP_FIELD) {
-        Some(v) => v.to_str().unwrap_or_default().to_string(),
-        None => req
-            .peer_addr()
-            .map(|addr| addr.ip().to_string())
-            .unwrap_or_default(),
-    };
-
-    let user_agent = req
-        .headers()
-        .get(header::USER_AGENT)
-        .map(|v| v.to_str().unwrap_or_default())
-        .unwrap_or_default()
-        .to_string();
-
-    (ip, user_agent)
-}
-
-async fn get_current_user(
-    req: &HttpRequest,
-    connection: web::Data<db::Connection>,
-) -> Option<model::User> {
-    let (client_ip, client_user_agent) = get_ip_and_user_agent(req);
-
-    match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
-        Some(token_cookie) => match connection
-            .authentication_async(token_cookie.value(), &client_ip, &client_user_agent)
-            .await
-        {
-            Ok(db::AuthenticationResult::NotValidToken) =>
-            // TODO: remove cookie?
-            {
-                None
-            }
-            Ok(db::AuthenticationResult::Ok(user_id)) => {
-                match connection.load_user_async(user_id).await {
-                    Ok(user) => Some(user),
-                    Err(error) => {
-                        error!("Error during authentication: {}", error);
-                        None
-                    }
-                }
-            }
-            Err(error) => {
-                error!("Error during authentication: {}", error);
-                None
-            }
-        },
-        None => None,
-    }
-}
-
-type Result<T> = std::result::Result<T, ServiceError>;
-
-///// ERROR /////
-
-#[derive(Debug)]
-pub struct ServiceError {
-    status_code: StatusCode,
-    message: Option<String>,
-}
-
-impl From<asynchronous::DBAsyncError> for ServiceError {
-    fn from(error: asynchronous::DBAsyncError) -> Self {
-        ServiceError {
-            status_code: StatusCode::INTERNAL_SERVER_ERROR,
-            message: Some(format!("{:?}", error)),
-        }
-    }
-}
-
-impl From<email::Error> for ServiceError {
-    fn from(error: email::Error) -> Self {
-        ServiceError {
-            status_code: StatusCode::INTERNAL_SERVER_ERROR,
-            message: Some(format!("{:?}", error)),
-        }
-    }
-}
-
-impl From<actix_web::error::BlockingError> for ServiceError {
-    fn from(error: actix_web::error::BlockingError) -> Self {
-        ServiceError {
-            status_code: StatusCode::INTERNAL_SERVER_ERROR,
-            message: Some(format!("{:?}", error)),
-        }
-    }
-}
-
-impl From<ron::error::SpannedError> for ServiceError {
-    fn from(error: ron::error::SpannedError) -> Self {
-        ServiceError {
-            status_code: StatusCode::INTERNAL_SERVER_ERROR,
-            message: Some(format!("{:?}", error)),
-        }
-    }
-}
-
-impl std::fmt::Display for ServiceError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
-        if let Some(ref m) = self.message {
-            write!(f, "**{}**\n\n", m)?;
-        }
-        write!(f, "Code: {}", self.status_code)
-    }
-}
-
-impl actix_web::error::ResponseError for ServiceError {
-    fn error_response(&self) -> HttpResponse {
-        MessageBaseTemplate {
+impl axum::response::IntoResponse for db::DBError {
+    fn into_response(self) -> Response {
+        let body = MessageTemplate {
+            user: None,
             message: &self.to_string(),
-        }
-        .to_response()
-    }
-
-    fn status_code(&self) -> StatusCode {
-        self.status_code
+        };
+        (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
     }
 }
 
@@ -153,20 +37,18 @@ struct HomeTemplate {
     current_recipe_id: Option<i64>,
 }
 
-#[get("/")]
+#[debug_handler]
 pub async fn home_page(
-    req: HttpRequest,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    let user = get_current_user(&req, connection.clone()).await;
-    let recipes = connection.get_all_recipe_titles_async().await?;
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+) -> Result<impl IntoResponse> {
+    let recipes = connection.get_all_recipe_titles().await?;
 
     Ok(HomeTemplate {
         user,
         current_recipe_id: None,
         recipes,
-    }
-    .to_response())
+    })
 }
 
 ///// VIEW RECIPE /////
@@ -177,115 +59,117 @@ struct ViewRecipeTemplate {
     user: Option<model::User>,
     recipes: Vec<(i64, String)>,
     current_recipe_id: Option<i64>,
-
     current_recipe: model::Recipe,
 }
 
-#[get("/recipe/view/{id}")]
+#[debug_handler]
 pub async fn view_recipe(
-    req: HttpRequest,
-    path: web::Path<(i64,)>,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    let (id,) = path.into_inner();
-    let user = get_current_user(&req, connection.clone()).await;
-    let recipes = connection.get_all_recipe_titles_async().await?;
-    let recipe = connection.get_recipe_async(id).await?;
-
-    Ok(ViewRecipeTemplate {
-        user,
-        current_recipe_id: Some(recipe.id),
-        recipes,
-        current_recipe: recipe,
-    }
-    .to_response())
-}
-
-///// EDIT/NEW RECIPE /////
-
-#[derive(Template)]
-#[template(path = "edit_recipe.html")]
-struct EditRecipeTemplate {
-    user: Option<model::User>,
-    recipes: Vec<(i64, String)>,
-    current_recipe_id: Option<i64>,
-
-    current_recipe: model::Recipe,
-}
-
-#[get("/recipe/edit/{id}")]
-pub async fn edit_recipe(
-    req: HttpRequest,
-    path: web::Path<(i64,)>,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    let (id,) = path.into_inner();
-    let user = match get_current_user(&req, connection.clone()).await {
-        Some(u) => u,
-        None => {
-            return Ok(MessageTemplate {
-                user: None,
-                message: "Cannot edit a recipe without being logged in",
-            }
-            .to_response())
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    Path(recipe_id): Path<i64>,
+) -> Result<Response> {
+    let recipes = connection.get_all_recipe_titles().await?;
+    match connection.get_recipe(recipe_id).await? {
+        Some(recipe) => Ok(ViewRecipeTemplate {
+            user,
+            current_recipe_id: Some(recipe.id),
+            recipes,
+            current_recipe: recipe,
         }
-    };
-
-    let recipe = connection.get_recipe_async(id).await?;
-
-    if recipe.user_id != user.id {
-        return Ok(MessageTemplate {
-            message: "Cannot edit a recipe you don't own",
-            user: Some(user),
+        .into_response()),
+        None => Ok(MessageTemplate {
+            user,
+            message: &format!("Cannot find the recipe {}", recipe_id),
         }
-        .to_response());
-    }
-
-    let recipes = connection.get_all_recipe_titles_async().await?;
-
-    Ok(EditRecipeTemplate {
-        user: Some(user),
-        current_recipe_id: Some(recipe.id),
-        recipes,
-        current_recipe: recipe,
+        .into_response()),
     }
-    .to_response())
 }
 
-#[get("/recipe/new")]
-pub async fn new_recipe(
-    req: HttpRequest,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    let user = match get_current_user(&req, connection.clone()).await {
-        Some(u) => u,
-        None => {
-            return Ok(MessageTemplate {
-                message: "Cannot create a recipe without being logged in",
-                user: None,
-            }
-            .to_response())
-        }
-    };
+///// EDIT/NEW RECIPE /////
 
-    let recipe_id = connection.create_recipe_async(user.id).await?;
-    let recipes = connection.get_all_recipe_titles_async().await?;
-    let user_id = user.id;
-
-    Ok(EditRecipeTemplate {
-        user: Some(user),
-        current_recipe_id: Some(recipe_id),
-        recipes,
-        current_recipe: model::Recipe::empty(recipe_id, user_id),
-    }
-    .to_response())
-}
+// #[derive(Template)]
+// #[template(path = "edit_recipe.html")]
+// struct EditRecipeTemplate {
+//     user: Option<model::User>,
+//     recipes: Vec<(i64, String)>,
+//     current_recipe_id: Option<i64>,
+
+//     current_recipe: model::Recipe,
+// }
+
+// #[get("/recipe/edit/{id}")]
+// pub async fn edit_recipe(
+//     req: HttpRequest,
+//     path: web::Path<(i64,)>,
+//     connection: web::Data<db::Connection>,
+// ) -> Result<HttpResponse> {
+//     let (id,) = path.into_inner();
+//     let user = match get_current_user(&req, connection.clone()).await {
+//         Some(u) => u,
+//         None => {
+//             return Ok(MessageTemplate {
+//                 user: None,
+//                 message: "Cannot edit a recipe without being logged in",
+//             }
+//             .to_response())
+//         }
+//     };
+
+//     let recipe = connection.get_recipe_async(id).await?;
+
+//     if recipe.user_id != user.id {
+//         return Ok(MessageTemplate {
+//             message: "Cannot edit a recipe you don't own",
+//             user: Some(user),
+//         }
+//         .to_response());
+//     }
+
+//     let recipes = connection.get_all_recipe_titles_async().await?;
+
+//     Ok(EditRecipeTemplate {
+//         user: Some(user),
+//         current_recipe_id: Some(recipe.id),
+//         recipes,
+//         current_recipe: recipe,
+//     }
+//     .to_response())
+// }
+
+// #[get("/recipe/new")]
+// pub async fn new_recipe(
+//     req: HttpRequest,
+//     connection: web::Data<db::Connection>,
+// ) -> Result<HttpResponse> {
+//     let user = match get_current_user(&req, connection.clone()).await {
+//         Some(u) => u,
+//         None => {
+//             return Ok(MessageTemplate {
+//                 message: "Cannot create a recipe without being logged in",
+//                 user: None,
+//             }
+//             .to_response())
+//         }
+//     };
+
+//     let recipe_id = connection.create_recipe_async(user.id).await?;
+//     let recipes = connection.get_all_recipe_titles_async().await?;
+//     let user_id = user.id;
+
+//     Ok(EditRecipeTemplate {
+//         user: Some(user),
+//         current_recipe_id: Some(recipe_id),
+//         recipes,
+//         current_recipe: model::Recipe::empty(recipe_id, user_id),
+//     }
+//     .to_response())
+// }
 
 ///// MESSAGE /////
 
 #[derive(Template)]
-#[template(path = "message_base.html")]
-struct MessageBaseTemplate<'a> {
+#[template(path = "message_without_user.html")]
+struct MessageWithoutUser<'a> {
     message: &'a str,
 }
 
@@ -308,22 +192,20 @@ struct SignUpFormTemplate {
     message_password: String,
 }
 
-#[get("/signup")]
+#[debug_handler]
 pub async fn sign_up_get(
-    req: HttpRequest,
-    connection: web::Data<db::Connection>,
-) -> impl Responder {
-    let user = get_current_user(&req, connection.clone()).await;
-    SignUpFormTemplate {
+    Extension(user): Extension<Option<model::User>>,
+) -> Result<impl IntoResponse> {
+    Ok(SignUpFormTemplate {
         user,
         email: String::new(),
         message: String::new(),
         message_email: String::new(),
         message_password: String::new(),
-    }
+    })
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Debug)]
 pub struct SignUpFormData {
     email: String,
     password_1: String,
@@ -339,21 +221,22 @@ enum SignUpError {
     UnableSendEmail,
 }
 
-#[post("/signup")]
+#[debug_handler(state = AppState)]
 pub async fn sign_up_post(
-    req: HttpRequest,
-    form: web::Form<SignUpFormData>,
-    connection: web::Data<db::Connection>,
-    config: web::Data<Config>,
-) -> Result<HttpResponse> {
+    Host(host): Host,
+    State(connection): State<db::Connection>,
+    State(config): State<Config>,
+    Extension(user): Extension<Option<model::User>>,
+    Form(form_data): Form<SignUpFormData>,
+) -> Result<Response> {
     fn error_response(
         error: SignUpError,
-        form: &web::Form<SignUpFormData>,
+        form_data: &SignUpFormData,
         user: Option<model::User>,
-    ) -> Result<HttpResponse> {
+    ) -> Result<Response> {
         Ok(SignUpFormTemplate {
             user,
-            email: form.email.clone(),
+            email: form_data.email.clone(),
             message_email: match error {
                 SignUpError::InvalidEmail => "Invalid email",
                 _ => "",
@@ -373,40 +256,35 @@ pub async fn sign_up_post(
             }
             .to_string(),
         }
-        .to_response())
+        .into_response())
     }
 
-    let user = get_current_user(&req, connection.clone()).await;
-
     // Validation of email and password.
-    if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form.email) {
-        return error_response(SignUpError::InvalidEmail, &form, user);
+    if let common::utils::EmailValidation::NotValid =
+        common::utils::validate_email(&form_data.email)
+    {
+        return error_response(SignUpError::InvalidEmail, &form_data, user);
     }
 
-    if form.password_1 != form.password_2 {
-        return error_response(SignUpError::PasswordsNotEqual, &form, user);
+    if form_data.password_1 != form_data.password_2 {
+        return error_response(SignUpError::PasswordsNotEqual, &form_data, user);
     }
 
     if let common::utils::PasswordValidation::TooShort =
-        common::utils::validate_password(&form.password_1)
+        common::utils::validate_password(&form_data.password_1)
     {
-        return error_response(SignUpError::InvalidPassword, &form, user);
+        return error_response(SignUpError::InvalidPassword, &form_data, user);
     }
 
     match connection
-        .sign_up_async(&form.email, &form.password_1)
+        .sign_up(&form_data.email, &form_data.password_1)
         .await
     {
         Ok(db::SignUpResult::UserAlreadyExists) => {
-            error_response(SignUpError::UserAlreadyExists, &form, user)
+            error_response(SignUpError::UserAlreadyExists, &form_data, user)
         }
         Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => {
             let url = {
-                let host = req
-                    .headers()
-                    .get(header::HOST)
-                    .map(|v| v.to_str().unwrap_or_default())
-                    .unwrap_or_default();
                 let port: Option<u16> = 'p: {
                     let split_port: Vec<&str> = host.split(':').collect();
                     if split_port.len() == 2 {
@@ -427,60 +305,61 @@ pub async fn sign_up_post(
                 )
             };
 
-            let email = form.email.clone();
-
-            match web::block(move || {
-                email::send_validation(
-                    &url,
-                    &email,
-                    &token,
-                    &config.smtp_login,
-                    &config.smtp_password,
-                )
-            })
-            .await?
+            println!("{}", &url);
+
+            let email = form_data.email.clone();
+            match email::send_validation(
+                &url,
+                &email,
+                &token,
+                &config.smtp_relay_address,
+                &config.smtp_login,
+                &config.smtp_password,
+            )
+            .await
             {
-                Ok(()) => Ok(HttpResponse::Found()
-                    .insert_header((header::LOCATION, "/signup_check_email"))
-                    .finish()),
-                Err(error) => {
-                    error!("Email validation error: {}", error);
-                    error_response(SignUpError::UnableSendEmail, &form, user)
+                Ok(()) => Ok(MessageTemplate {
+                    user,
+                    message: "An email has been sent, follow the link to validate your account.",
+                }
+                .into_response()),
+                Err(_) => {
+                    // error!("Email validation error: {}", error); // TODO: log
+                    error_response(SignUpError::UnableSendEmail, &form_data, user)
                 }
             }
         }
-        Err(error) => {
-            error!("Signup database error: {}", error);
-            error_response(SignUpError::DatabaseError, &form, user)
+        Err(_) => {
+            // error!("Signup database error: {}", error);
+            error_response(SignUpError::DatabaseError, &form_data, user)
         }
     }
 }
 
-#[get("/signup_check_email")]
-pub async fn sign_up_check_email(
-    req: HttpRequest,
-    connection: web::Data<db::Connection>,
-) -> impl Responder {
-    let user = get_current_user(&req, connection.clone()).await;
-    MessageTemplate {
-        user,
-        message: "An email has been sent, follow the link to validate your account.",
-    }
-}
-
-#[get("/validation")]
+#[debug_handler]
 pub async fn sign_up_validation(
-    req: HttpRequest,
-    query: web::Query<HashMap<String, String>>,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
-    let user = get_current_user(&req, connection.clone()).await;
-
-    match query.get("token") {
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ConnectInfo(addr): ConnectInfo<SocketAddr>,
+    Query(query): Query<HashMap<String, String>>,
+    headers: HeaderMap,
+) -> Result<(CookieJar, impl IntoResponse)> {
+    let mut jar = CookieJar::from_headers(&headers);
+    if user.is_some() {
+        return Ok((
+            jar,
+            MessageTemplate {
+                user,
+                message: "User already exists",
+            },
+        ));
+    }
+    let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
+    match query.get("validation_token") {
+        // 'validation_token' exists only when a user tries to validate a new account.
         Some(token) => {
             match connection
-                .validation_async(
+                .validation(
                     token,
                     Duration::seconds(consts::VALIDATION_TOKEN_DURATION),
                     &client_ip,
@@ -490,43 +369,39 @@ pub async fn sign_up_validation(
             {
                 db::ValidationResult::Ok(token, user_id) => {
                     let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
-                    let user = match connection.load_user(user_id) {
-                        Ok(user) => Some(user),
-                        Err(error) => {
-                            error!("Error retrieving user by id: {}", error);
-                            None
-                        }
-                    };
-
-                    let mut response = MessageTemplate {
-                        user,
-                        message: "Email validation successful, your account has been created",
-                    }
-                    .to_response();
-
-                    if let Err(error) = response.add_cookie(&cookie) {
-                        error!("Unable to set cookie after validation: {}", error);
-                    };
-
-                    Ok(response)
-                }
-                db::ValidationResult::ValidationExpired => Ok(MessageTemplate {
-                    user,
-                    message: "The validation has expired. Try to sign up again.",
-                }
-                .to_response()),
-                db::ValidationResult::UnknownUser => Ok(MessageTemplate {
-                    user,
-                    message: "Validation error.",
+                    jar = jar.add(cookie);
+                    let user = connection.load_user(user_id).await?;
+                    Ok((
+                        jar,
+                        MessageTemplate {
+                            user,
+                            message: "Email validation successful, your account has been created",
+                        },
+                    ))
                 }
-                .to_response()),
+                db::ValidationResult::ValidationExpired => Ok((
+                    jar,
+                    MessageTemplate {
+                        user,
+                        message: "The validation has expired. Try to sign up again",
+                    },
+                )),
+                db::ValidationResult::UnknownUser => Ok((
+                    jar,
+                    MessageTemplate {
+                        user,
+                        message: "Validation error. Try to sign up again",
+                    },
+                )),
             }
         }
-        None => Ok(MessageTemplate {
-            user,
-            message: &format!("No token provided"),
-        }
-        .to_response()),
+        None => Ok((
+            jar,
+            MessageTemplate {
+                user,
+                message: "Validation error",
+            },
+        )),
     }
 }
 
@@ -540,109 +415,89 @@ struct SignInFormTemplate {
     message: String,
 }
 
-#[get("/signin")]
+#[debug_handler]
 pub async fn sign_in_get(
-    req: HttpRequest,
-    connection: web::Data<db::Connection>,
-) -> impl Responder {
-    let user = get_current_user(&req, connection.clone()).await;
-    SignInFormTemplate {
+    Extension(user): Extension<Option<model::User>>,
+) -> Result<impl IntoResponse> {
+    Ok(SignInFormTemplate {
         user,
         email: String::new(),
         message: String::new(),
-    }
+    })
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Debug)]
 pub struct SignInFormData {
     email: String,
     password: String,
 }
 
-enum SignInError {
-    AccountNotValidated,
-    AuthenticationFailed,
-}
-
-#[post("/signin")]
+#[debug_handler]
 pub async fn sign_in_post(
-    req: HttpRequest,
-    form: web::Form<SignInFormData>,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    fn error_response(
-        error: SignInError,
-        form: &web::Form<SignInFormData>,
-        user: Option<model::User>,
-    ) -> Result<HttpResponse> {
-        Ok(SignInFormTemplate {
-            user,
-            email: form.email.clone(),
-            message: match error {
-                SignInError::AccountNotValidated => "This account must be validated first",
-                SignInError::AuthenticationFailed => "Wrong email or password",
-            }
-            .to_string(),
-        }
-        .to_response())
-    }
-
-    let user = get_current_user(&req, connection.clone()).await;
-    let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
+    ConnectInfo(addr): ConnectInfo<SocketAddr>,
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    headers: HeaderMap,
+    Form(form_data): Form<SignInFormData>,
+) -> Result<(CookieJar, Response)> {
+    let jar = CookieJar::from_headers(&headers);
+    let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
 
     match connection
-        .sign_in_async(&form.email, &form.password, &client_ip, &client_user_agent)
-        .await
+        .sign_in(
+            &form_data.email,
+            &form_data.password,
+            &client_ip,
+            &client_user_agent,
+        )
+        .await?
     {
-        Ok(db::SignInResult::AccountNotValidated) => {
-            error_response(SignInError::AccountNotValidated, &form, user)
-        }
-        Ok(db::SignInResult::UserNotFound) | Ok(db::SignInResult::WrongPassword) => {
-            error_response(SignInError::AuthenticationFailed, &form, user)
-        }
-        Ok(db::SignInResult::Ok(token, user_id)) => {
+        db::SignInResult::AccountNotValidated => Ok((
+            jar,
+            SignInFormTemplate {
+                user,
+                email: form_data.email,
+                message: "This account must be validated first".to_string(),
+            }
+            .into_response(),
+        )),
+        db::SignInResult::UserNotFound | db::SignInResult::WrongPassword => Ok((
+            jar,
+            SignInFormTemplate {
+                user,
+                email: form_data.email,
+                message: "Wrong email or password".to_string(),
+            }
+            .into_response(),
+        )),
+        db::SignInResult::Ok(token, _user_id) => {
             let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
-            let mut response = HttpResponse::Found()
-                .insert_header((header::LOCATION, "/"))
-                .finish();
-            if let Err(error) = response.add_cookie(&cookie) {
-                error!("Unable to set cookie after sign in: {}", error);
-            };
-            Ok(response)
-        }
-        Err(error) => {
-            error!("Signin error: {}", error);
-            error_response(SignInError::AuthenticationFailed, &form, user)
+            Ok((jar.add(cookie), Redirect::to("/").into_response()))
         }
     }
 }
 
 ///// SIGN OUT /////
 
-#[get("/signout")]
-pub async fn sign_out(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
-    let mut response = HttpResponse::Found()
-        .insert_header((header::LOCATION, "/"))
-        .finish();
-
-    if let Some(token_cookie) = req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
-        if let Err(error) = connection.sign_out_async(token_cookie.value()).await {
-            error!("Unable to sign out: {}", error);
-        };
-
-        if let Err(error) =
-            response.add_removal_cookie(&Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, ""))
-        {
-            error!("Unable to set a removal cookie after sign out: {}", error);
-        };
-    };
-    response
+#[debug_handler]
+pub async fn sign_out(
+    State(connection): State<db::Connection>,
+    req: Request<Body>,
+) -> Result<(CookieJar, Redirect)> {
+    let mut jar = CookieJar::from_headers(req.headers());
+    if let Some(token_cookie) = jar.get(consts::COOKIE_AUTH_TOKEN_NAME) {
+        let token = token_cookie.value().to_string();
+        jar = jar.remove(consts::COOKIE_AUTH_TOKEN_NAME);
+        connection.sign_out(&token).await?;
+    }
+    Ok((jar, Redirect::to("/")))
 }
 
-pub async fn not_found(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
-    let user = get_current_user(&req, connection.clone()).await;
-    MessageTemplate {
-        user,
+///// 404 /////
+
+#[debug_handler]
+pub async fn not_found() -> Result<impl IntoResponse> {
+    Ok(MessageWithoutUser {
         message: "404: Not found",
-    }
+    })
 }
index c72d96d..93836a2 100644 (file)
@@ -1,38 +1,38 @@
-use actix_web::{
-    http::{header, header::ContentType, StatusCode},
-    post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder,
-};
-use log::{debug, error, info, log_enabled, Level};
-use ron::de::from_bytes;
+// use actix_web::{
+//     http::{header, header::ContentType, StatusCode},
+//     post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder,
+// };
+// use log::{debug, error, info, log_enabled, Level};
+// use ron::de::from_bytes;
 
-use super::Result;
-use crate::data::{asynchronous, db};
+// use super::Result;
+// use crate::data::db;
 
-#[put("/ron-api/recipe/set-title")]
-pub async fn set_recipe_title(
-    req: HttpRequest,
-    body: web::Bytes,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    let ron_req: common::ron_api::SetRecipeTitle = from_bytes(&body)?;
-    connection
-        .set_recipe_title_async(ron_req.recipe_id, &ron_req.title)
-        .await?;
-    Ok(HttpResponse::Ok().finish())
-}
+// #[put("/ron-api/recipe/set-title")]
+// pub async fn set_recipe_title(
+//     req: HttpRequest,
+//     body: web::Bytes,
+//     connection: web::Data<db::Connection>,
+// ) -> Result<HttpResponse> {
+//     let ron_req: common::ron_api::SetRecipeTitle = from_bytes(&body)?;
+//     connection
+//         .set_recipe_title_async(ron_req.recipe_id, &ron_req.title)
+//         .await?;
+//     Ok(HttpResponse::Ok().finish())
+// }
 
-#[put("/ron-api/recipe/set-description")]
-pub async fn set_recipe_description(
-    req: HttpRequest,
-    body: web::Bytes,
-    connection: web::Data<db::Connection>,
-) -> Result<HttpResponse> {
-    let ron_req: common::ron_api::SetRecipeDescription = from_bytes(&body)?;
-    connection
-        .set_recipe_description_async(ron_req.recipe_id, &ron_req.description)
-        .await?;
-    Ok(HttpResponse::Ok().finish())
-}
+// #[put("/ron-api/recipe/set-description")]
+// pub async fn set_recipe_description(
+//     req: HttpRequest,
+//     body: web::Bytes,
+//     connection: web::Data<db::Connection>,
+// ) -> Result<HttpResponse> {
+//     let ron_req: common::ron_api::SetRecipeDescription = from_bytes(&body)?;
+//     connection
+//         .set_recipe_description_async(ron_req.recipe_id, &ron_req.description)
+//         .await?;
+//     Ok(HttpResponse::Ok().finish())
+// }
 
 // #[put("/ron-api/recipe/add-image)]
 // #[put("/ron-api/recipe/rm-photo")]
index f8d41af..3885ce9 100644 (file)
@@ -1,11 +1,20 @@
-use log::error;
-
-pub fn unwrap_print_err<T, E>(r: Result<T, E>) -> T
-where
-    E: std::fmt::Debug,
-{
-    if let Err(ref error) = r {
-        error!("{:?}", error);
-    }
-    r.unwrap()
+use std::net::SocketAddr;
+
+use axum::http::HeaderMap;
+
+use crate::consts;
+
+pub fn get_ip_and_user_agent(headers: &HeaderMap, remote_address: SocketAddr) -> (String, String) {
+    let ip = match headers.get(consts::REVERSE_PROXY_IP_HTTP_FIELD) {
+        Some(v) => v.to_str().unwrap_or_default().to_string(),
+        None => remote_address.to_string(),
+    };
+
+    let user_agent = headers
+        .get(axum::http::header::USER_AGENT)
+        .map(|v| v.to_str().unwrap_or_default())
+        .unwrap_or_default()
+        .to_string();
+
+    (ip, user_agent)
 }
index 9cff88c..ea4a50d 100644 (file)
@@ -2,7 +2,7 @@
 
 {% block body_container %}
     <div class="header-container">
-        <a class="title" href="/">~~ Recettes de cuisine ~~</a>
+        {% include "title.html" %}
 
         {% match user %}
         {% when Some with (user) %}
index cccc4e4..95d3874 100644 (file)
@@ -1,7 +1,6 @@
 {% extends "base_with_header.html" %}
 
 {% block main_container %}
-
-{{ message|markdown }}
-
+    {{ message|markdown }}
+    <a href="/">Go to home</a>
 {% endblock %}
\ No newline at end of file
diff --git a/backend/templates/message_base.html b/backend/templates/message_base.html
deleted file mode 100644 (file)
index f73b85f..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends "base.html" %}
-
-{% block body_container %}
-{% include "title.html" %}
-{{ message|markdown }}
-{% endblock %}
\ No newline at end of file
diff --git a/backend/templates/message_without_user.html b/backend/templates/message_without_user.html
new file mode 100644 (file)
index 0000000..ee42a33
--- /dev/null
@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+
+{% block body_container %}
+    {% include "title.html" %}
+    {{ message|markdown }}
+{% endblock %}
\ No newline at end of file
index 2a4715c..33f7014 100644 (file)
@@ -1 +1 @@
-<h1><a href="/">~~ Recettes de cuisine ~~</a></h1>
\ No newline at end of file
+<a class="title" href="/">~~ Recettes de cuisine ~~</a>
\ No newline at end of file
diff --git a/check_cargo_dependencies_upgrade.nu b/check_cargo_dependencies_upgrade.nu
new file mode 100644 (file)
index 0000000..7ada4aa
--- /dev/null
@@ -0,0 +1,2 @@
+# It needs cargo-edit: https://crates.io/crates/cargo-edit .
+cargo upgrade --dry-run --verbose
\ No newline at end of file
index 94cf1a5..2a89f96 100644 (file)
@@ -1,2 +1,2 @@
+pub mod ron_api;
 pub mod utils;
-pub mod ron_api;
\ No newline at end of file
index 566e430..1d50b12 100644 (file)
@@ -1,4 +1,3 @@
-use ron::de::from_reader;
 use serde::{Deserialize, Serialize};
 
 #[derive(Serialize, Deserialize, Clone)]
index 94b8c38..9322ed6 100644 (file)
@@ -1,5 +1,5 @@
-use regex::Regex;
 use lazy_static::lazy_static;
+use regex::Regex;
 
 pub enum EmailValidation {
     Ok,
@@ -7,11 +7,18 @@ pub enum EmailValidation {
 }
 
 lazy_static! {
-    static ref EMAIL_REGEX: Regex = Regex::new(r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})").expect("Error parsing email regex");
+    static ref EMAIL_REGEX: Regex = Regex::new(
+        r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})"
+    )
+    .expect("Error parsing email regex");
 }
 
 pub fn validate_email(email: &str) -> EmailValidation {
-    if EMAIL_REGEX.is_match(email) { EmailValidation::Ok } else { EmailValidation::NotValid }
+    if EMAIL_REGEX.is_match(email) {
+        EmailValidation::Ok
+    } else {
+        EmailValidation::NotValid
+    }
 }
 
 pub enum PasswordValidation {
@@ -20,5 +27,9 @@ pub enum PasswordValidation {
 }
 
 pub fn validate_password(password: &str) -> PasswordValidation {
-    if password.len() < 8 { PasswordValidation::TooShort } else { PasswordValidation::Ok }
-}
\ No newline at end of file
+    if password.len() < 8 {
+        PasswordValidation::TooShort
+    } else {
+        PasswordValidation::Ok
+    }
+}
index 2e46351..3cb718a 100644 (file)
--- a/deploy.nu
+++ b/deploy.nu
@@ -1,18 +1,20 @@
 def main [host: string, destination: string, ssh_key: path] {
     let ssh_args = [-i $ssh_key $host]
     let scp_args = [-r -i $ssh_key]
-    let target = "aarch64-unknown-linux-gnu" # For raspberry pi zero 1: "arm-unknown-linux-gnueabihf"
+
+    # For raspberry pi zero 1: "arm-unknown-linux-gnueabihf"
+    let target = "aarch64-unknown-linux-gnu"
 
     def invoke_ssh [command: list] {
         let args = $ssh_args ++ $command
         print $"Executing: ssh ($args)"
-        ssh $args
+        ssh ...$args
     }
 
     def copy_ssh [source: string, destination: string] {
         let args = $scp_args ++ [$source $"($host):($destination)"]
         print $"Executing: scp ($args)"
-        scp $args
+        scp ...$args
     }
 
     cargo build --target $target --release
@@ -25,4 +27,3 @@ def main [host: string, destination: string, ssh_key: path] {
     invoke_ssh [sudo systemctl start recipes]
     print "Deployment finished"
 }
-
index 56462f2..c555ff7 100644 (file)
@@ -11,21 +11,29 @@ crate-type = ["cdylib"]
 default = ["console_error_panic_hook"]
 
 [dependencies]
-common = {path = "../common"}
+common = { path = "../common" }
 
 wasm-bindgen = "0.2"
-web-sys = {version = "0.3", features = ['console', 'Document', 'Element', 'HtmlElement', 'Node', 'Window', 'Location']}
+web-sys = { version = "0.3", features = [
+    'console',
+    'Document',
+    'Element',
+    'HtmlElement',
+    'Node',
+    'Window',
+    'Location',
+] }
 
 # The `console_error_panic_hook` crate provides better debugging of panics by
 # logging them with `console.error`. This is great for development, but requires
 # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
 # code size when deploying.
-console_error_panic_hook = {version = "0.1", optional = true}
+console_error_panic_hook = { version = "0.1", optional = true }
 
 # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
 # compared to the default allocator's ~10K. It is slower than the default
 # allocator, however.
-wee_alloc = {version = "0.4", optional = true}
+wee_alloc = { version = "0.4", optional = true }
 
 # [dev-dependencies]
 # wasm-bindgen-test = "0.3"