diff --git a/.gitignore b/.gitignore
index ea8c4bf..6f88073 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
-/target
+*.env
+package-lock.json
+node_modules
+database.sqlite
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
deleted file mode 100644
index 4397a09..0000000
--- a/Cargo.lock
+++ /dev/null
@@ -1,4102 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "actix-codec"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
-dependencies = [
- "bitflags 2.6.0",
- "bytes",
- "futures-core",
- "futures-sink",
- "memchr",
- "pin-project-lite",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "actix-cors"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370"
-dependencies = [
- "actix-utils",
- "actix-web",
- "derive_more",
- "futures-util",
- "log",
- "once_cell",
- "smallvec",
-]
-
-[[package]]
-name = "actix-files"
-version = "0.6.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be"
-dependencies = [
- "actix-http",
- "actix-service",
- "actix-utils",
- "actix-web",
- "bitflags 2.6.0",
- "bytes",
- "derive_more",
- "futures-core",
- "http-range",
- "log",
- "mime",
- "mime_guess",
- "percent-encoding",
- "pin-project-lite",
- "v_htmlescape",
-]
-
-[[package]]
-name = "actix-http"
-version = "3.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4"
-dependencies = [
- "actix-codec",
- "actix-rt",
- "actix-service",
- "actix-utils",
- "ahash 0.8.11",
- "base64 0.22.1",
- "bitflags 2.6.0",
- "brotli",
- "bytes",
- "bytestring",
- "derive_more",
- "encoding_rs",
- "flate2",
- "futures-core",
- "h2",
- "http 0.2.12",
- "httparse",
- "httpdate",
- "itoa",
- "language-tags",
- "local-channel",
- "mime",
- "percent-encoding",
- "pin-project-lite",
- "rand",
- "sha1",
- "smallvec",
- "tokio",
- "tokio-util",
- "tracing",
- "zstd",
-]
-
-[[package]]
-name = "actix-macros"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
-dependencies = [
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "actix-router"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
-dependencies = [
- "bytestring",
- "cfg-if",
- "http 0.2.12",
- "regex",
- "regex-lite",
- "serde",
- "tracing",
-]
-
-[[package]]
-name = "actix-rt"
-version = "2.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
-dependencies = [
- "actix-macros",
- "futures-core",
- "tokio",
-]
-
-[[package]]
-name = "actix-server"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894"
-dependencies = [
- "actix-rt",
- "actix-service",
- "actix-utils",
- "futures-core",
- "futures-util",
- "mio",
- "socket2 0.5.9",
- "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.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38"
-dependencies = [
- "actix-codec",
- "actix-http",
- "actix-macros",
- "actix-router",
- "actix-rt",
- "actix-server",
- "actix-service",
- "actix-utils",
- "actix-web-codegen",
- "ahash 0.8.11",
- "bytes",
- "bytestring",
- "cfg-if",
- "cookie",
- "derive_more",
- "encoding_rs",
- "futures-core",
- "futures-util",
- "impl-more",
- "itoa",
- "language-tags",
- "log",
- "mime",
- "once_cell",
- "pin-project-lite",
- "regex",
- "regex-lite",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "smallvec",
- "socket2 0.5.9",
- "time",
- "url",
-]
-
-[[package]]
-name = "actix-web-codegen"
-version = "4.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
-dependencies = [
- "actix-router",
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "actix_route_rate_limiter"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77495de640f6247d4d2d7ef34a98573e20edf9eab03914902ae965ca5c06c1f4"
-dependencies = [
- "actix-service",
- "actix-web",
- "chrono",
- "futures",
- "log",
- "rand",
- "tokio",
-]
-
-[[package]]
-name = "addr2line"
-version = "0.24.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
-dependencies = [
- "gimli",
-]
-
-[[package]]
-name = "adler2"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
-
-[[package]]
-name = "ahash"
-version = "0.7.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
-dependencies = [
- "getrandom 0.2.15",
- "once_cell",
- "version_check",
-]
-
-[[package]]
-name = "ahash"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
-dependencies = [
- "cfg-if",
- "getrandom 0.2.15",
- "once_cell",
- "version_check",
- "zerocopy",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "1.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "alloc-no-stdlib"
-version = "2.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
-
-[[package]]
-name = "alloc-stdlib"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
-dependencies = [
- "alloc-no-stdlib",
-]
-
-[[package]]
-name = "allocator-api2"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-
-[[package]]
-name = "android-tzdata"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
-
-[[package]]
-name = "android_system_properties"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "anyhow"
-version = "1.0.95"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
-
-[[package]]
-name = "async-trait"
-version = "0.1.88"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "autocfg"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
-
-[[package]]
-name = "backtrace"
-version = "0.3.74"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
-dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "base64"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-
-[[package]]
-name = "base64"
-version = "0.21.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
-
-[[package]]
-name = "base64"
-version = "0.22.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
-
-[[package]]
-name = "bcrypt"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641"
-dependencies = [
- "base64 0.13.1",
- "blowfish",
- "getrandom 0.2.15",
- "zeroize",
-]
-
-[[package]]
-name = "bitflags"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
-
-[[package]]
-name = "bitflags"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
-
-[[package]]
-name = "block-buffer"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "blowfish"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
-dependencies = [
- "byteorder",
- "cipher",
-]
-
-[[package]]
-name = "brotli"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
- "brotli-decompressor",
-]
-
-[[package]]
-name = "brotli-decompressor"
-version = "4.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
-]
-
-[[package]]
-name = "bumpalo"
-version = "3.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
-
-[[package]]
-name = "byteorder"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
-
-[[package]]
-name = "bytes"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
-
-[[package]]
-name = "bytestring"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f"
-dependencies = [
- "bytes",
-]
-
-[[package]]
-name = "cc"
-version = "1.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
-dependencies = [
- "jobserver",
- "libc",
- "shlex",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-
-[[package]]
-name = "chrono"
-version = "0.4.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
-dependencies = [
- "android-tzdata",
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "serde",
- "wasm-bindgen",
- "windows-link",
-]
-
-[[package]]
-name = "cipher"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
-dependencies = [
- "crypto-common",
- "inout",
-]
-
-[[package]]
-name = "config"
-version = "0.13.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca"
-dependencies = [
- "async-trait",
- "json5",
- "lazy_static",
- "nom",
- "pathdiff",
- "ron",
- "rust-ini",
- "serde",
- "serde_json",
- "toml",
- "yaml-rust",
-]
-
-[[package]]
-name = "convert_case"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
-
-[[package]]
-name = "cookie"
-version = "0.16.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
-dependencies = [
- "percent-encoding",
- "time",
- "version_check",
-]
-
-[[package]]
-name = "core-foundation"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-
-[[package]]
-name = "cpufeatures"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crc32fast"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "crossbeam-channel"
-version = "0.5.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
-
-[[package]]
-name = "crypto-common"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
-dependencies = [
- "generic-array",
- "typenum",
-]
-
-[[package]]
-name = "cssparser"
-version = "0.31.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be"
-dependencies = [
- "cssparser-macros",
- "dtoa-short",
- "itoa",
- "phf 0.11.3",
- "smallvec",
-]
-
-[[package]]
-name = "cssparser-macros"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
-dependencies = [
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "debugid"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
-dependencies = [
- "serde",
- "uuid",
-]
-
-[[package]]
-name = "deranged"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
-dependencies = [
- "powerfmt",
-]
-
-[[package]]
-name = "derive_more"
-version = "0.99.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
-dependencies = [
- "convert_case",
- "proc-macro2",
- "quote",
- "rustc_version",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "digest"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
-dependencies = [
- "block-buffer",
- "crypto-common",
-]
-
-[[package]]
-name = "displaydoc"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "dlv-list"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
-
-[[package]]
-name = "dotenv"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
-
-[[package]]
-name = "dtoa"
-version = "1.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
-
-[[package]]
-name = "dtoa-short"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
-dependencies = [
- "dtoa",
-]
-
-[[package]]
-name = "ego-tree"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12a0bb14ac04a9fcf170d0bbbef949b44cc492f4452bd20c095636956f653642"
-
-[[package]]
-name = "email-encoding"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a87260449b06739ee78d6281c68d2a0ff3e3af64a78df63d3a1aeb3c06997c8a"
-dependencies = [
- "base64 0.22.1",
- "memchr",
-]
-
-[[package]]
-name = "email_address"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
-
-[[package]]
-name = "encoding_rs"
-version = "0.8.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "env_logger"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
-dependencies = [
- "humantime",
- "is-terminal",
- "log",
- "regex",
- "termcolor",
-]
-
-[[package]]
-name = "equivalent"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
-
-[[package]]
-name = "errno"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
-dependencies = [
- "libc",
- "windows-sys 0.59.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"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
-
-[[package]]
-name = "fastrand"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
-dependencies = [
- "instant",
-]
-
-[[package]]
-name = "fastrand"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
-
-[[package]]
-name = "findshlibs"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
-dependencies = [
- "cc",
- "lazy_static",
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "flate2"
-version = "1.0.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
-dependencies = [
- "crc32fast",
- "miniz_oxide",
-]
-
-[[package]]
-name = "fnv"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-
-[[package]]
-name = "foreign-types"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
-
-[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-
-[[package]]
-name = "form_urlencoded"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
-dependencies = [
- "percent-encoding",
-]
-
-[[package]]
-name = "formies_be"
-version = "0.1.0"
-dependencies = [
- "actix-cors",
- "actix-files",
- "actix-http",
- "actix-rt",
- "actix-web",
- "actix_route_rate_limiter",
- "anyhow",
- "bcrypt",
- "chrono",
- "config",
- "dotenv",
- "env_logger",
- "futures",
- "lettre",
- "log",
- "regex",
- "reqwest 0.11.27",
- "rusqlite",
- "scraper",
- "sentry",
- "serde",
- "serde_json",
- "tracing",
- "tracing-actix-web",
- "tracing-appender",
- "tracing-bunyan-formatter",
- "tracing-log 0.2.0",
- "tracing-subscriber",
- "ureq",
- "url",
- "uuid",
- "validator",
-]
-
-[[package]]
-name = "futf"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
-dependencies = [
- "mac",
- "new_debug_unreachable",
-]
-
-[[package]]
-name = "futures"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
-
-[[package]]
-name = "futures-macro"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "futures-sink"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
-
-[[package]]
-name = "futures-task"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
-
-[[package]]
-name = "futures-util"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project-lite",
- "pin-utils",
- "slab",
-]
-
-[[package]]
-name = "fxhash"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
-dependencies = [
- "byteorder",
-]
-
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
-]
-
-[[package]]
-name = "gethostname"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
-dependencies = [
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "getopts"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
-dependencies = [
- "unicode-width",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
-dependencies = [
- "cfg-if",
- "libc",
- "r-efi",
- "wasi 0.14.2+wasi-0.2.4",
-]
-
-[[package]]
-name = "gimli"
-version = "0.31.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-
-[[package]]
-name = "h2"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
-dependencies = [
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "futures-util",
- "http 0.2.12",
- "indexmap",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-dependencies = [
- "ahash 0.7.8",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.14.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
-dependencies = [
- "ahash 0.8.11",
- "allocator-api2",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
-
-[[package]]
-name = "hashlink"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
-dependencies = [
- "hashbrown 0.14.5",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
-
-[[package]]
-name = "hex"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
-
-[[package]]
-name = "hostname"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
-dependencies = [
- "libc",
- "match_cfg",
- "winapi",
-]
-
-[[package]]
-name = "hostname"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
-dependencies = [
- "cfg-if",
- "libc",
- "windows-link",
-]
-
-[[package]]
-name = "html5ever"
-version = "0.26.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
-dependencies = [
- "log",
- "mac",
- "markup5ever",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "http"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http"
-version = "1.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http-body"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
-dependencies = [
- "bytes",
- "http 0.2.12",
- "pin-project-lite",
-]
-
-[[package]]
-name = "http-body"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
-dependencies = [
- "bytes",
- "http 1.3.1",
-]
-
-[[package]]
-name = "http-body-util"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
-dependencies = [
- "bytes",
- "futures-core",
- "http 1.3.1",
- "http-body 1.0.1",
- "pin-project-lite",
-]
-
-[[package]]
-name = "http-range"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
-
-[[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 = "humantime"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
-
-[[package]]
-name = "hyper"
-version = "0.14.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-core",
- "futures-util",
- "h2",
- "http 0.2.12",
- "http-body 0.4.6",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "socket2 0.5.9",
- "tokio",
- "tower-service",
- "tracing",
- "want",
-]
-
-[[package]]
-name = "hyper"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "http 1.3.1",
- "http-body 1.0.1",
- "httparse",
- "itoa",
- "pin-project-lite",
- "smallvec",
- "tokio",
- "want",
-]
-
-[[package]]
-name = "hyper-tls"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
-dependencies = [
- "bytes",
- "hyper 0.14.32",
- "native-tls",
- "tokio",
- "tokio-native-tls",
-]
-
-[[package]]
-name = "hyper-tls"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
-dependencies = [
- "bytes",
- "http-body-util",
- "hyper 1.6.0",
- "hyper-util",
- "native-tls",
- "tokio",
- "tokio-native-tls",
- "tower-service",
-]
-
-[[package]]
-name = "hyper-util"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "http 1.3.1",
- "http-body 1.0.1",
- "hyper 1.6.0",
- "libc",
- "pin-project-lite",
- "socket2 0.5.9",
- "tokio",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "iana-time-zone"
-version = "0.1.63"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "icu_collections"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
-dependencies = [
- "displaydoc",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
-
-[[package]]
-name = "icu_normalizer"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
-
-[[package]]
-name = "icu_properties"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_locid_transform",
- "icu_properties_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_properties_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
-
-[[package]]
-name = "icu_provider"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_provider_macros",
- "stable_deref_trait",
- "tinystr",
- "writeable",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_provider_macros"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "idna"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
-dependencies = [
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[package]]
-name = "idna"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
-dependencies = [
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[package]]
-name = "idna"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
-dependencies = [
- "idna_adapter",
- "smallvec",
- "utf8_iter",
-]
-
-[[package]]
-name = "idna_adapter"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
-dependencies = [
- "icu_normalizer",
- "icu_properties",
-]
-
-[[package]]
-name = "if_chain"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
-
-[[package]]
-name = "impl-more"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
-
-[[package]]
-name = "indexmap"
-version = "2.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
-dependencies = [
- "equivalent",
- "hashbrown 0.15.2",
-]
-
-[[package]]
-name = "inout"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "instant"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "ipnet"
-version = "2.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
-
-[[package]]
-name = "is-terminal"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
-dependencies = [
- "hermit-abi",
- "libc",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "itoa"
-version = "1.0.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
-
-[[package]]
-name = "jobserver"
-version = "0.1.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "js-sys"
-version = "0.3.77"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
-dependencies = [
- "once_cell",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "json5"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
-dependencies = [
- "pest",
- "pest_derive",
- "serde",
-]
-
-[[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.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
-[[package]]
-name = "lettre"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d"
-dependencies = [
- "async-trait",
- "base64 0.21.7",
- "email-encoding",
- "email_address",
- "fastrand 1.9.0",
- "futures-io",
- "futures-util",
- "hostname 0.3.1",
- "httpdate",
- "idna 0.3.0",
- "mime",
- "native-tls",
- "nom",
- "once_cell",
- "quoted_printable",
- "socket2 0.4.10",
- "tokio",
- "tokio-native-tls",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.172"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
-
-[[package]]
-name = "libsqlite3-sys"
-version = "0.26.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
-dependencies = [
- "cc",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "linked-hash-map"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
-
-[[package]]
-name = "litemap"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
-
-[[package]]
-name = "local-channel"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
-dependencies = [
- "futures-core",
- "futures-sink",
- "local-waker",
-]
-
-[[package]]
-name = "local-waker"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
-
-[[package]]
-name = "lock_api"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
-
-[[package]]
-name = "mac"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
-
-[[package]]
-name = "markup5ever"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
-dependencies = [
- "log",
- "phf 0.10.1",
- "phf_codegen",
- "string_cache",
- "string_cache_codegen",
- "tendril",
-]
-
-[[package]]
-name = "match_cfg"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
-
-[[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
-[[package]]
-name = "memchr"
-version = "2.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
-
-[[package]]
-name = "mime"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
-
-[[package]]
-name = "mime_guess"
-version = "2.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
-dependencies = [
- "mime",
- "unicase",
-]
-
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
-dependencies = [
- "adler2",
-]
-
-[[package]]
-name = "mio"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
-dependencies = [
- "libc",
- "log",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "mutually_exclusive_features"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577"
-
-[[package]]
-name = "native-tls"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
-dependencies = [
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
-]
-
-[[package]]
-name = "new_debug_unreachable"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
-
-[[package]]
-name = "nom"
-version = "7.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
-[[package]]
-name = "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-conv"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
-
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "object"
-version = "0.36.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
-
-[[package]]
-name = "openssl"
-version = "0.10.72"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
-dependencies = [
- "bitflags 2.6.0",
- "cfg-if",
- "foreign-types",
- "libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "openssl-probe"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.108"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "ordered-multimap"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
-dependencies = [
- "dlv-list",
- "hashbrown 0.12.3",
-]
-
-[[package]]
-name = "os_info"
-version = "3.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a604e53c24761286860eba4e2c8b23a0161526476b1de520139d69cdb85a6b5"
-dependencies = [
- "log",
- "serde",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "paste"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
-
-[[package]]
-name = "pathdiff"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
-
-[[package]]
-name = "percent-encoding"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-
-[[package]]
-name = "pest"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
-dependencies = [
- "memchr",
- "thiserror 2.0.12",
- "ucd-trie",
-]
-
-[[package]]
-name = "pest_derive"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
-dependencies = [
- "pest",
- "pest_generator",
-]
-
-[[package]]
-name = "pest_generator"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "pest_meta"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
-dependencies = [
- "once_cell",
- "pest",
- "sha2",
-]
-
-[[package]]
-name = "phf"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
-dependencies = [
- "phf_shared 0.10.0",
-]
-
-[[package]]
-name = "phf"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
-dependencies = [
- "phf_macros",
- "phf_shared 0.11.3",
-]
-
-[[package]]
-name = "phf_codegen"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
-dependencies = [
- "phf_generator 0.10.0",
- "phf_shared 0.10.0",
-]
-
-[[package]]
-name = "phf_generator"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
-dependencies = [
- "phf_shared 0.10.0",
- "rand",
-]
-
-[[package]]
-name = "phf_generator"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
-dependencies = [
- "phf_shared 0.11.3",
- "rand",
-]
-
-[[package]]
-name = "phf_macros"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
-dependencies = [
- "phf_generator 0.11.3",
- "phf_shared 0.11.3",
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "phf_shared"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
-dependencies = [
- "siphasher 0.3.11",
-]
-
-[[package]]
-name = "phf_shared"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
-dependencies = [
- "siphasher 1.0.1",
-]
-
-[[package]]
-name = "pin-project"
-version = "1.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
-dependencies = [
- "pin-project-internal",
-]
-
-[[package]]
-name = "pin-project-internal"
-version = "1.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-
-[[package]]
-name = "pkg-config"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
-
-[[package]]
-name = "powerfmt"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
-
-[[package]]
-name = "ppv-lite86"
-version = "0.2.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
-dependencies = [
- "zerocopy",
-]
-
-[[package]]
-name = "precomputed-hash"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
-
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "quoted_printable"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
-
-[[package]]
-name = "r-efi"
-version = "5.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
-
-[[package]]
-name = "rand"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
-dependencies = [
- "libc",
- "rand_chacha",
- "rand_core",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
-dependencies = [
- "ppv-lite86",
- "rand_core",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
-dependencies = [
- "getrandom 0.2.15",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
-dependencies = [
- "bitflags 2.6.0",
-]
-
-[[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.9",
- "regex-syntax 0.8.5",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax 0.8.5",
-]
-
-[[package]]
-name = "regex-lite"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
-
-[[package]]
-name = "regex-syntax"
-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 = "reqwest"
-version = "0.11.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
-dependencies = [
- "base64 0.21.7",
- "bytes",
- "encoding_rs",
- "futures-core",
- "futures-util",
- "h2",
- "http 0.2.12",
- "http-body 0.4.6",
- "hyper 0.14.32",
- "hyper-tls 0.5.0",
- "ipnet",
- "js-sys",
- "log",
- "mime",
- "native-tls",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "rustls-pemfile 1.0.4",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper 0.1.2",
- "system-configuration",
- "tokio",
- "tokio-native-tls",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "winreg",
-]
-
-[[package]]
-name = "reqwest"
-version = "0.12.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-channel",
- "futures-core",
- "futures-util",
- "http 1.3.1",
- "http-body 1.0.1",
- "http-body-util",
- "hyper 1.6.0",
- "hyper-tls 0.6.0",
- "hyper-util",
- "ipnet",
- "js-sys",
- "log",
- "mime",
- "native-tls",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "rustls-pemfile 2.2.0",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper 1.0.2",
- "tokio",
- "tokio-native-tls",
- "tower",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "windows-registry",
-]
-
-[[package]]
-name = "ring"
-version = "0.17.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
-dependencies = [
- "cc",
- "cfg-if",
- "getrandom 0.2.15",
- "libc",
- "untrusted",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "ron"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
-dependencies = [
- "base64 0.13.1",
- "bitflags 1.3.2",
- "serde",
-]
-
-[[package]]
-name = "rusqlite"
-version = "0.29.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
-dependencies = [
- "bitflags 2.6.0",
- "chrono",
- "fallible-iterator",
- "fallible-streaming-iterator",
- "hashlink",
- "libsqlite3-sys",
- "smallvec",
-]
-
-[[package]]
-name = "rust-ini"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
-dependencies = [
- "cfg-if",
- "ordered-multimap",
-]
-
-[[package]]
-name = "rustc-demangle"
-version = "0.1.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
-
-[[package]]
-name = "rustc_version"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
-dependencies = [
- "semver",
-]
-
-[[package]]
-name = "rustix"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
-dependencies = [
- "bitflags 2.6.0",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "rustls"
-version = "0.23.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
-dependencies = [
- "log",
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "rustls-pemfile"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
-dependencies = [
- "base64 0.21.7",
-]
-
-[[package]]
-name = "rustls-pemfile"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
-dependencies = [
- "rustls-pki-types",
-]
-
-[[package]]
-name = "rustls-pki-types"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
-
-[[package]]
-name = "rustls-webpki"
-version = "0.103.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
-dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
-
-[[package]]
-name = "ryu"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
-
-[[package]]
-name = "schannel"
-version = "0.1.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
-dependencies = [
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
-name = "scraper"
-version = "0.18.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1"
-dependencies = [
- "ahash 0.8.11",
- "cssparser",
- "ego-tree",
- "getopts",
- "html5ever",
- "once_cell",
- "selectors",
- "tendril",
-]
-
-[[package]]
-name = "security-framework"
-version = "2.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
-dependencies = [
- "bitflags 2.6.0",
- "core-foundation",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework-sys"
-version = "2.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "selectors"
-version = "0.25.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06"
-dependencies = [
- "bitflags 2.6.0",
- "cssparser",
- "derive_more",
- "fxhash",
- "log",
- "new_debug_unreachable",
- "phf 0.10.1",
- "phf_codegen",
- "precomputed-hash",
- "servo_arc",
- "smallvec",
-]
-
-[[package]]
-name = "semver"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
-
-[[package]]
-name = "sentry"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "255914a8e53822abd946e2ce8baa41d4cded6b8e938913b7f7b9da5b7ab44335"
-dependencies = [
- "httpdate",
- "native-tls",
- "reqwest 0.12.15",
- "sentry-backtrace",
- "sentry-contexts",
- "sentry-core",
- "sentry-debug-images",
- "sentry-log",
- "sentry-panic",
- "sentry-tracing",
- "tokio",
- "ureq",
-]
-
-[[package]]
-name = "sentry-backtrace"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00293cd332a859961f24fd69258f7e92af736feaeb91020cff84dac4188a4302"
-dependencies = [
- "backtrace",
- "once_cell",
- "regex",
- "sentry-core",
-]
-
-[[package]]
-name = "sentry-contexts"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "961990f9caa76476c481de130ada05614cd7f5aa70fb57c2142f0e09ad3fb2aa"
-dependencies = [
- "hostname 0.4.1",
- "libc",
- "os_info",
- "rustc_version",
- "sentry-core",
- "uname",
-]
-
-[[package]]
-name = "sentry-core"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a6409d845707d82415c800290a5d63be5e3df3c2e417b0997c60531dfbd35ef"
-dependencies = [
- "once_cell",
- "rand",
- "sentry-types",
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "sentry-debug-images"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71ab5df4f3b64760508edfe0ba4290feab5acbbda7566a79d72673065888e5cc"
-dependencies = [
- "findshlibs",
- "once_cell",
- "sentry-core",
-]
-
-[[package]]
-name = "sentry-log"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693841da8dfb693af29105edfbea1d91348a13d23dd0a5d03761eedb9e450c46"
-dependencies = [
- "log",
- "sentry-core",
-]
-
-[[package]]
-name = "sentry-panic"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "609b1a12340495ce17baeec9e08ff8ed423c337c1a84dffae36a178c783623f3"
-dependencies = [
- "sentry-backtrace",
- "sentry-core",
-]
-
-[[package]]
-name = "sentry-tracing"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f4e86402d5c50239dc7d8fd3f6d5e048221d5fcb4e026d8d50ab57fe4644cb"
-dependencies = [
- "sentry-backtrace",
- "sentry-core",
- "tracing-core",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "sentry-types"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3f117b8755dbede8260952de2aeb029e20f432e72634e8969af34324591631"
-dependencies = [
- "debugid",
- "hex",
- "rand",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "time",
- "url",
- "uuid",
-]
-
-[[package]]
-name = "serde"
-version = "1.0.216"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.216"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.134"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
-dependencies = [
- "itoa",
- "memchr",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "servo_arc"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44"
-dependencies = [
- "stable_deref_trait",
-]
-
-[[package]]
-name = "sha1"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "sha2"
-version = "0.10.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
-dependencies = [
- "cfg-if",
- "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.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "siphasher"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
-
-[[package]]
-name = "siphasher"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
-
-[[package]]
-name = "slab"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "smallvec"
-version = "1.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
-
-[[package]]
-name = "socket2"
-version = "0.4.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
-dependencies = [
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "socket2"
-version = "0.5.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
-dependencies = [
- "libc",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
-
-[[package]]
-name = "string_cache"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
-dependencies = [
- "new_debug_unreachable",
- "parking_lot",
- "phf_shared 0.11.3",
- "precomputed-hash",
- "serde",
-]
-
-[[package]]
-name = "string_cache_codegen"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
-dependencies = [
- "phf_generator 0.11.3",
- "phf_shared 0.11.3",
- "proc-macro2",
- "quote",
-]
-
-[[package]]
-name = "subtle"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
-
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "sync_wrapper"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
-
-[[package]]
-name = "sync_wrapper"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
-dependencies = [
- "futures-core",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "system-configuration"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
-dependencies = [
- "bitflags 1.3.2",
- "core-foundation",
- "system-configuration-sys",
-]
-
-[[package]]
-name = "system-configuration-sys"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "tempfile"
-version = "3.19.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
-dependencies = [
- "fastrand 2.3.0",
- "getrandom 0.3.2",
- "once_cell",
- "rustix",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "tendril"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
-dependencies = [
- "futf",
- "mac",
- "utf-8",
-]
-
-[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
-]
-
-[[package]]
-name = "thiserror"
-version = "2.0.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
-dependencies = [
- "thiserror-impl 2.0.12",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "2.0.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "thread_local"
-version = "1.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
-
-[[package]]
-name = "time"
-version = "0.3.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
-dependencies = [
- "deranged",
- "itoa",
- "num-conv",
- "powerfmt",
- "serde",
- "time-core",
- "time-macros",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
-
-[[package]]
-name = "time-macros"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
-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.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "tokio"
-version = "1.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
-dependencies = [
- "backtrace",
- "bytes",
- "libc",
- "mio",
- "parking_lot",
- "pin-project-lite",
- "signal-hook-registry",
- "socket2 0.5.9",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "tokio-native-tls"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
-dependencies = [
- "native-tls",
- "tokio",
-]
-
-[[package]]
-name = "tokio-util"
-version = "0.7.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
-dependencies = [
- "bytes",
- "futures-core",
- "futures-sink",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "toml"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "tower"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
-dependencies = [
- "futures-core",
- "futures-util",
- "pin-project-lite",
- "sync_wrapper 1.0.2",
- "tokio",
- "tower-layer",
- "tower-service",
-]
-
-[[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.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
-dependencies = [
- "log",
- "pin-project-lite",
- "tracing-attributes",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-actix-web"
-version = "0.7.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2340b7722695166c7fc9b3e3cd1166e7c74fedb9075b8f0c74d3822d2e41caf5"
-dependencies = [
- "actix-web",
- "mutually_exclusive_features",
- "pin-project",
- "tracing",
- "uuid",
-]
-
-[[package]]
-name = "tracing-appender"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
-dependencies = [
- "crossbeam-channel",
- "thiserror 1.0.69",
- "time",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "tracing-attributes"
-version = "0.1.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "tracing-bunyan-formatter"
-version = "0.3.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d637245a0d8774bd48df6482e086c59a8b5348a910c3b0579354045a9d82411"
-dependencies = [
- "ahash 0.8.11",
- "gethostname",
- "log",
- "serde",
- "serde_json",
- "time",
- "tracing",
- "tracing-core",
- "tracing-log 0.1.4",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "tracing-core"
-version = "0.1.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
-dependencies = [
- "once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-log"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
-dependencies = [
- "log",
- "once_cell",
- "tracing-core",
-]
-
-[[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.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log 0.2.0",
-]
-
-[[package]]
-name = "try-lock"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
-
-[[package]]
-name = "typenum"
-version = "1.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
-
-[[package]]
-name = "ucd-trie"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
-
-[[package]]
-name = "uname"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "unicase"
-version = "2.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
-
-[[package]]
-name = "unicode-bidi"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
-
-[[package]]
-name = "unicode-normalization"
-version = "0.1.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "unicode-width"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
-
-[[package]]
-name = "untrusted"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
-
-[[package]]
-name = "ureq"
-version = "2.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
-dependencies = [
- "base64 0.22.1",
- "flate2",
- "log",
- "native-tls",
- "once_cell",
- "rustls",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "url",
- "webpki-roots",
-]
-
-[[package]]
-name = "url"
-version = "2.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
-dependencies = [
- "form_urlencoded",
- "idna 1.0.3",
- "percent-encoding",
- "serde",
-]
-
-[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
-[[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 = "uuid"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
-dependencies = [
- "getrandom 0.2.15",
- "serde",
-]
-
-[[package]]
-name = "v_htmlescape"
-version = "0.15.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
-
-[[package]]
-name = "validator"
-version = "0.16.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd"
-dependencies = [
- "idna 0.4.0",
- "lazy_static",
- "regex",
- "serde",
- "serde_derive",
- "serde_json",
- "url",
- "validator_derive",
-]
-
-[[package]]
-name = "validator_derive"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af"
-dependencies = [
- "if_chain",
- "lazy_static",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "regex",
- "syn 1.0.109",
- "validator_types",
-]
-
-[[package]]
-name = "validator_types"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3"
-dependencies = [
- "proc-macro2",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "valuable"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
-
-[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
-[[package]]
-name = "version_check"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
-
-[[package]]
-name = "want"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
-dependencies = [
- "try-lock",
-]
-
-[[package]]
-name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
-
-[[package]]
-name = "wasi"
-version = "0.14.2+wasi-0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
-dependencies = [
- "wit-bindgen-rt",
-]
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
-dependencies = [
- "cfg-if",
- "once_cell",
- "rustversion",
- "wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
-dependencies = [
- "bumpalo",
- "log",
- "proc-macro2",
- "quote",
- "syn 2.0.92",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-futures"
-version = "0.4.50"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
-dependencies = [
- "cfg-if",
- "js-sys",
- "once_cell",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "web-sys"
-version = "0.3.77"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "0.26.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93"
-dependencies = [
- "rustls-pki-types",
-]
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-util"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
-dependencies = [
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "windows-core"
-version = "0.61.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
-dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-link",
- "windows-result",
- "windows-strings 0.4.0",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.60.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.59.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
-]
-
-[[package]]
-name = "windows-link"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
-
-[[package]]
-name = "windows-registry"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
-dependencies = [
- "windows-result",
- "windows-strings 0.3.1",
- "windows-targets 0.53.0",
-]
-
-[[package]]
-name = "windows-result"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets 0.48.5",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "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.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
- "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-targets"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
-dependencies = [
- "windows_aarch64_gnullvm 0.53.0",
- "windows_aarch64_msvc 0.53.0",
- "windows_i686_gnu 0.53.0",
- "windows_i686_gnullvm 0.53.0",
- "windows_i686_msvc 0.53.0",
- "windows_x86_64_gnu 0.53.0",
- "windows_x86_64_gnullvm 0.53.0",
- "windows_x86_64_msvc 0.53.0",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
-
-[[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_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
-
-[[package]]
-name = "winreg"
-version = "0.50.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
-dependencies = [
- "cfg-if",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "wit-bindgen-rt"
-version = "0.39.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
-dependencies = [
- "bitflags 2.6.0",
-]
-
-[[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 = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
-
-[[package]]
-name = "yaml-rust"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
-dependencies = [
- "linked-hash-map",
-]
-
-[[package]]
-name = "yoke"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
-dependencies = [
- "serde",
- "stable_deref_trait",
- "yoke-derive",
- "zerofrom",
-]
-
-[[package]]
-name = "yoke-derive"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
- "synstructure",
-]
-
-[[package]]
-name = "zerocopy"
-version = "0.7.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
-dependencies = [
- "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 2.0.92",
-]
-
-[[package]]
-name = "zerofrom"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
-dependencies = [
- "zerofrom-derive",
-]
-
-[[package]]
-name = "zerofrom-derive"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.92",
- "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 2.0.92",
-]
-
-[[package]]
-name = "zstd"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
-dependencies = [
- "zstd-safe",
-]
-
-[[package]]
-name = "zstd-safe"
-version = "7.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
-dependencies = [
- "zstd-sys",
-]
-
-[[package]]
-name = "zstd-sys"
-version = "2.0.13+zstd.1.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
-dependencies = [
- "cc",
- "pkg-config",
-]
diff --git a/Cargo.toml b/Cargo.toml
deleted file mode 100644
index 2f94962..0000000
--- a/Cargo.toml
+++ /dev/null
@@ -1,39 +0,0 @@
-[package]
-name = "formies_be"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-actix-web = "4.0"
-rusqlite = { version = "0.29", features = ["bundled", "chrono"] }
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-uuid = { version = "1.0", features = ["v4"] }
-actix-files = "0.6"
-actix-cors = "0.6"
-env_logger = "0.10"
-log = "0.4"
-futures = "0.3"
-bcrypt = "0.13"
-anyhow = "1.0"
-dotenv = "0.15.0"
-chrono = { version = "0.4", features = ["serde"] }
-regex = "1"
-url = "2"
-reqwest = { version = "0.11", features = ["json"] }
-scraper = "0.18"
-lettre = { version = "0.10", features = ["builder", "tokio1-native-tls"] }
-ureq = { version = "2.9", features = ["json"] }
-# Production dependencies
-actix_route_rate_limiter = "0.2.2"
-actix-rt = "2.0"
-actix-http = "3.0"
-config = "0.13"
-sentry = { version = "0.37", features = ["log"] }
-validator = { version = "0.16", features = ["derive"] }
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-tracing-actix-web = "0.7"
-tracing-log = "0.2"
-tracing-appender = "0.2"
-tracing-bunyan-formatter = "0.3"
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index df502e7..575c4b8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,51 +1,36 @@
-# Build stage
-FROM rust:1.70-slim as builder
+FROM node:24-alpine AS builder
-WORKDIR /app
+WORKDIR /usr/src/app
-# Install build dependencies
-RUN apt-get update && apt-get install -y \
- pkg-config \
- libsqlite3-dev \
- && rm -rf /var/lib/apt/lists/*
+# Install build dependencies for sqlite3
+RUN apk add --no-cache python3 make g++ sqlite-dev
+
+COPY package*.json ./
+RUN npm ci
-# Copy source code
COPY . .
-# Build the application
-RUN cargo build --release
+FROM node:24-alpine
-# Runtime stage
-FROM debian:bullseye-slim
+WORKDIR /usr/src/app
-WORKDIR /app
+# Install runtime dependencies for sqlite3
+RUN apk add --no-cache sqlite-libs python3 make g++ sqlite-dev
-# Install runtime dependencies
-RUN apt-get update && apt-get install -y \
- libsqlite3-0 \
- ca-certificates \
- && rm -rf /var/lib/apt/lists/*
+# Create a non-root user
+RUN addgroup -S appgroup && adduser -S appuser -G appgroup
-# Create necessary directories
-RUN mkdir -p /app/data /app/logs
+COPY --from=builder /usr/src/app/package*.json ./
+COPY --from=builder /usr/src/app/ ./
-# Copy the binary from builder
-COPY --from=builder /app/target/release/formies-be /app/
+# Rebuild sqlite3 for the target architecture
+RUN npm rebuild sqlite3
-# Copy configuration
-COPY config/default.toml /app/config/default.toml
+# Set ownership to non-root user
+RUN chown -R appuser:appgroup /usr/src/app
-# Set environment variables
-ENV RUST_LOG=info
-ENV DATABASE_URL=/app/data/form_data.db
-ENV BIND_ADDRESS=0.0.0.0:8080
+USER appuser
-# Expose port
-EXPOSE 8080
+EXPOSE 3000
-# Set proper permissions
-RUN chown -R nobody:nogroup /app
-USER nobody
-
-# Run the application
-CMD ["./formies-be"]
\ No newline at end of file
+CMD ["node", "server.js"]
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index b74a4f2..0000000
--- a/README.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# Formies Backend
-
-A production-ready Rust backend for the Formies application.
-
-## Features
-
-- RESTful API endpoints
-- SQLite database with connection pooling
-- JWT-based authentication
-- Rate limiting
-- Structured logging
-- Error tracking with Sentry
-- Health check endpoint
-- CORS support
-- Configuration management
-- Metrics endpoint
-
-## Prerequisites
-
-- Rust 1.70 or later
-- SQLite 3
-- Make (optional, for using Makefile commands)
-
-## Configuration
-
-The application can be configured using environment variables or a configuration file. The following environment variables are supported:
-
-### Required Environment Variables
-
-- `DATABASE_URL`: SQLite database URL (default: form_data.db)
-- `BIND_ADDRESS`: Server bind address (default: 127.0.0.1:8080)
-- `INITIAL_ADMIN_USERNAME`: Initial admin username
-- `INITIAL_ADMIN_PASSWORD`: Initial admin password
-
-### Optional Environment Variables
-
-- `ALLOWED_ORIGIN`: CORS allowed origin
-- `RUST_LOG`: Log level (default: info)
-- `SENTRY_DSN`: Sentry DSN for error tracking
-- `JWT_SECRET`: JWT secret key
-- `JWT_EXPIRATION`: JWT expiration time in seconds
-
-## Development
-
-1. Clone the repository
-2. Install dependencies:
- ```bash
- cargo build
- ```
-3. Set up environment variables:
- ```bash
- cp .env.example .env
- # Edit .env with your configuration
- ```
-4. Run the development server:
- ```bash
- cargo run
- ```
-
-## Production Deployment
-
-### Docker
-
-1. Build the Docker image:
-
- ```bash
- docker build -t formies-backend .
- ```
-
-2. Run the container:
- ```bash
- docker run -d \
- --name formies-backend \
- -p 8080:8080 \
- -v $(pwd)/data:/app/data \
- -e DATABASE_URL=/app/data/form_data.db \
- -e BIND_ADDRESS=0.0.0.0:8080 \
- -e INITIAL_ADMIN_USERNAME=admin \
- -e INITIAL_ADMIN_PASSWORD=your-secure-password \
- -e ALLOWED_ORIGIN=https://your-frontend-domain.com \
- -e SENTRY_DSN=your-sentry-dsn \
- formies-backend
- ```
-
-### Systemd Service
-
-1. Create a systemd service file at `/etc/systemd/system/formies-backend.service`:
-
- ```ini
- [Unit]
- Description=Formies Backend Service
- After=network.target
-
- [Service]
- Type=simple
- User=formies
- WorkingDirectory=/opt/formies-backend
- ExecStart=/opt/formies-backend/formies-be
- Restart=always
- Environment=DATABASE_URL=/opt/formies-backend/data/form_data.db
- Environment=BIND_ADDRESS=0.0.0.0:8080
- Environment=INITIAL_ADMIN_USERNAME=admin
- Environment=INITIAL_ADMIN_PASSWORD=your-secure-password
- Environment=ALLOWED_ORIGIN=https://your-frontend-domain.com
- Environment=SENTRY_DSN=your-sentry-dsn
-
- [Install]
- WantedBy=multi-user.target
- ```
-
-2. Enable and start the service:
- ```bash
- sudo systemctl enable formies-backend
- sudo systemctl start formies-backend
- ```
-
-## Monitoring
-
-### Health Check
-
-The application exposes a health check endpoint at `/api/health`:
-
-```bash
-curl http://localhost:8080/api/health
-```
-
-### Metrics
-
-Metrics are available at `/metrics` when enabled in the configuration.
-
-### Logging
-
-Logs are written to the configured log file and can be viewed using:
-
-```bash
-tail -f logs/app.log
-```
-
-## Security
-
-- All API endpoints are rate-limited
-- CORS is configured to only allow specified origins
-- JWT tokens are used for authentication
-- Passwords are hashed using bcrypt
-- SQLite database is protected with proper file permissions
-
-## License
-
-MIT
diff --git a/config/default.toml b/config/default.toml
deleted file mode 100644
index 8100cb5..0000000
--- a/config/default.toml
+++ /dev/null
@@ -1,30 +0,0 @@
-[server]
-bind_address = "127.0.0.1:8080"
-workers = 4
-keep_alive = 60
-client_timeout = 5000
-client_shutdown = 5000
-
-[database]
-url = "form_data.db"
-pool_size = 5
-connection_timeout = 30
-
-[security]
-rate_limit_requests = 100
-rate_limit_interval = 60
-allowed_origins = ["http://localhost:5173"]
-jwt_secret = "your-secret-key"
-jwt_expiration = 3600
-
-[logging]
-level = "info"
-format = "json"
-file = "logs/app.log"
-max_size = 10485760 # 10MB
-max_files = 5
-
-[monitoring]
-sentry_dsn = ""
-enable_metrics = true
-metrics_port = 9090
\ No newline at end of file
diff --git a/design.html b/design.html
deleted file mode 100644
index cdf8d88..0000000
--- a/design.html
+++ /dev/null
@@ -1,1294 +0,0 @@
-
-
-
-
-
- FormCraft - Scandinavian Industrial Form Management
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Total Submissions
-
1,248
-
-
-
-
- 12% from last month
-
-
-
-
Active Forms
-
24
-
-
-
-
- 3 new this month
-
-
-
-
Avg. Conversion Rate
-
68.4%
-
-
-
-
- 2.1% from last month
-
-
-
-
Storage Used
-
342 MB
-
-
-
-
-
-
- 24 MB from last month
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..cb503ed
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,13 @@
+services:
+ app:
+ image: whtvrboo/formies:1.02
+ container_name: formies
+ ports:
+ - "3000:3000"
+ environment:
+ - NODE_ENV=development
+ - NTFY_TOPIC_URL=https://ntfy.vinylnostalgia.com/form-alerts
+ - NTFY_ENABLED=true
+ - PORT=3000
+ volumes:
+ - ./database.sqlite:/usr/src/app/database.sqlite
diff --git a/form_data.db b/form_data.db
deleted file mode 100644
index a2884b8..0000000
Binary files a/form_data.db and /dev/null differ
diff --git a/frontend/index.html b/frontend/index.html
deleted file mode 100644
index e7f8172..0000000
--- a/frontend/index.html
+++ /dev/null
@@ -1,220 +0,0 @@
-
-
-
-
-
- Formies
-
-
-
-
-
-
-
-
-
-
-
Formies - Simple Form Manager
-
-
-
-
-
-
-
-
-
-
-
Admin Panel
-
-
-
-
-
-
-
Existing Forms
-
-
- Load Forms
-
-
-
-
-
-
-
- Submissions for
-
-
-
-
-
-
-
-
-
Submit to a Form
-
Enter a Form ID to load and submit:
-
-
-
- Load Form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
×
-
Notification Settings for
-
-
-
-
-
-
-
diff --git a/frontend/script.js b/frontend/script.js
deleted file mode 100644
index 210a7a9..0000000
--- a/frontend/script.js
+++ /dev/null
@@ -1,575 +0,0 @@
-document.addEventListener("DOMContentLoaded", () => {
- // --- Configuration ---
- const API_BASE_URL = "http://localhost:8080/api"; // Assuming backend serves API under /api
-
- // --- State ---
- let authToken = sessionStorage.getItem("authToken"); // Use sessionStorage for non-persistent login
-
- // --- DOM Elements ---
- const loginSection = document.getElementById("login-section");
- const adminSection = document.getElementById("admin-section");
- const loginForm = document.getElementById("login-form");
- const usernameInput = document.getElementById("username");
- const passwordInput = document.getElementById("password");
- const logoutButton = document.getElementById("logout-button");
- const statusArea = document.getElementById("status-area");
- const loggedInUserSpan = document.getElementById("logged-in-user"); // Added this if needed
-
- const createForm = document.getElementById("create-form");
- const formNameInput = document.getElementById("form-name");
-
- const loadFormsButton = document.getElementById("load-forms-button");
- const formsList = document.getElementById("forms-list");
-
- const submissionsSection = document.getElementById("submissions-section");
- const submissionsList = document.getElementById("submissions-list");
- const submissionsFormNameSpan = document.getElementById(
- "submissions-form-name"
- );
-
- const publicFormIdInput = document.getElementById("public-form-id-input");
- const loadPublicFormButton = document.getElementById(
- "load-public-form-button"
- );
- const publicFormArea = document.getElementById("public-form-area");
- const publicFormTitle = document.getElementById("public-form-title");
- const publicForm = document.getElementById("public-form");
-
- // --- Helper Functions ---
- function showStatus(message, isError = false) {
- statusArea.textContent = message;
- statusArea.className = "status"; // Reset classes
- if (message) {
- statusArea.classList.add(isError ? "error" : "success");
- }
- }
-
- function toggleSections() {
- console.log("toggleSections called. Current authToken:", authToken); // Log 3
- if (authToken) {
- console.log("AuthToken found, showing admin section."); // Log 4
- loginSection.classList.add("hidden");
- adminSection.classList.remove("hidden");
- // Optionally display username if you fetch it after login
- // loggedInUserSpan.textContent = 'Admin'; // Placeholder
- } else {
- console.log("AuthToken not found, showing login section."); // Log 5
- loginSection.classList.remove("hidden");
- adminSection.classList.add("hidden");
- submissionsSection.classList.add("hidden"); // Hide submissions when logged out
- }
- // Always hide public form initially on state change
- publicFormArea.classList.add("hidden");
- publicForm.innerHTML = 'Submit Form '; // Reset form content
- }
-
- async function makeApiRequest(
- endpoint,
- method = "GET",
- body = null,
- requiresAuth = false
- ) {
- const url = `${API_BASE_URL}${endpoint}`;
- const headers = {
- "Content-Type": "application/json",
- Accept: "application/json",
- };
-
- if (requiresAuth) {
- if (!authToken) {
- throw new Error("Authentication required, but no token found.");
- }
- headers["Authorization"] = `Bearer ${authToken}`;
- }
-
- const options = {
- method,
- headers,
- };
-
- if (body) {
- options.body = JSON.stringify(body);
- }
-
- try {
- const response = await fetch(url, options);
-
- if (!response.ok) {
- let errorData;
- try {
- errorData = await response.json(); // Try to parse error JSON
- } catch (e) {
- // If response is not JSON
- errorData = {
- message: `HTTP Error: ${response.status} ${response.statusText}`,
- };
- }
- // Check for backend's validation error structure
- if (errorData && errorData.validation_errors) {
- throw { validationErrors: errorData.validation_errors };
- }
- // Throw a more generic error message or the one from backend if available
- throw new Error(
- errorData.message || `Request failed with status ${response.status}`
- );
- }
-
- // Handle responses with no content (e.g., logout)
- if (
- response.status === 204 ||
- response.headers.get("content-length") === "0"
- ) {
- return null; // Or return an empty object/success indicator
- }
-
- return await response.json(); // Parse successful JSON response
- } catch (error) {
- console.error(`API Request Error (${method} ${endpoint}):`, error);
- // Re-throw validation errors specifically if they exist
- if (error.validationErrors) {
- throw error;
- }
- // Re-throw other errors
- throw new Error(error.message || "Network error or failed to fetch");
- }
- }
-
- // --- Event Handlers ---
- loginForm.addEventListener("submit", async (e) => {
- e.preventDefault();
- showStatus(""); // Clear previous status
- const username = usernameInput.value.trim();
- const password = passwordInput.value.trim();
-
- if (!username || !password) {
- showStatus("Username and password are required.", true);
- return;
- }
-
- try {
- const data = await makeApiRequest("/login", "POST", {
- username,
- password,
- });
- if (data && data.token) {
- console.log("Login successful, received token:", data.token); // Log 1
- authToken = data.token;
- sessionStorage.setItem("authToken", authToken); // Store token
- console.log("Calling toggleSections after login..."); // Log 2
- toggleSections();
- showStatus("Login successful!");
- usernameInput.value = ""; // Clear fields
- passwordInput.value = "";
- } else {
- throw new Error("Login failed: No token received.");
- }
- } catch (error) {
- showStatus(`Login failed: ${error.message}`, true);
- authToken = null;
- sessionStorage.removeItem("authToken");
- toggleSections();
- }
- });
-
- logoutButton.addEventListener("click", async () => {
- showStatus("");
- if (!authToken) return;
-
- try {
- await makeApiRequest("/logout", "POST", null, true);
- showStatus("Logout successful!");
- } catch (error) {
- showStatus(`Logout failed: ${error.message}`, true);
- // Decide if you still want to clear local state even if server fails
- // Forcing logout locally might be better UX in case of server error
- } finally {
- // Always clear local state on logout attempt
- authToken = null;
- sessionStorage.removeItem("authToken");
- toggleSections();
- }
- });
-
- if (createForm) {
- createForm.addEventListener("submit", async (e) => {
- e.preventDefault();
- showStatus("");
- const formName = formNameInput.value.trim();
- if (!formName) {
- showStatus("Please enter a form name", true);
- return;
- }
-
- try {
- // Refactor to use makeApiRequest
- const data = await makeApiRequest(
- "/forms", // Endpoint relative to API_BASE_URL
- "POST",
- // TODO: Need a way to define form fields in the UI.
- // Sending minimal structure for now.
- { name: formName, fields: [] },
- true // Requires authentication
- );
-
- if (!data || !data.id) {
- throw new Error(
- "Failed to create form or received invalid response."
- );
- }
-
- showStatus(
- `Form '${data.name}' created successfully! (ID: ${data.id})`,
- "success"
- );
- formNameInput.value = "";
- // Automatically refresh the forms list after creation
- if (loadFormsButton) {
- loadFormsButton.click();
- }
- } catch (error) {
- showStatus(`Error creating form: ${error.message}`, true);
- }
- });
- }
-
- // Ensure createFormFromUrl exists before adding listener
- const createFormFromUrlEl = document.getElementById("create-form-from-url");
- if (createFormFromUrlEl) {
- // Check if the element exists
- const formNameUrlInput = document.getElementById("form-name-url");
- const formUrlInput = document.getElementById("form-url");
-
- createFormFromUrlEl.addEventListener("submit", async (e) => {
- e.preventDefault();
- showStatus("");
- const name = formNameUrlInput.value.trim();
- const url = formUrlInput.value.trim();
-
- if (!name || !url) {
- showStatus("Form name and URL are required.", true);
- return;
- }
-
- try {
- const newForm = await makeApiRequest(
- "/forms/from-url",
- "POST",
- { name, url },
- true
- );
- showStatus(
- `Form '${newForm.name}' created successfully with ID: ${newForm.id}`
- );
- formNameUrlInput.value = ""; // Clear form
- formUrlInput.value = "";
- loadFormsButton.click(); // Refresh the forms list
- } catch (error) {
- showStatus(`Failed to create form from URL: ${error.message}`, true);
- }
- });
- }
-
- if (loadFormsButton) {
- loadFormsButton.addEventListener("click", async () => {
- showStatus("");
- submissionsSection.classList.add("hidden"); // Hide submissions when reloading forms
- formsList.innerHTML = "Loading... "; // Indicate loading
-
- try {
- const forms = await makeApiRequest("/forms", "GET", null, true);
- formsList.innerHTML = ""; // Clear list
-
- if (forms && forms.length > 0) {
- forms.forEach((form) => {
- const li = document.createElement("li");
- li.textContent = `${form.name} (ID: ${form.id})`;
-
- const viewSubmissionsButton = document.createElement("button");
- viewSubmissionsButton.textContent = "View Submissions";
- viewSubmissionsButton.onclick = () =>
- loadSubmissions(form.id, form.name);
-
- li.appendChild(viewSubmissionsButton);
- formsList.appendChild(li);
- });
- } else {
- formsList.innerHTML = "No forms found. ";
- }
- } catch (error) {
- showStatus(`Failed to load forms: ${error.message}`, true);
- formsList.innerHTML = "Error loading forms. ";
- }
- });
- }
-
- async function loadSubmissions(formId, formName) {
- showStatus("");
- submissionsList.innerHTML = "Loading submissions... ";
- submissionsFormNameSpan.textContent = `${formName} (ID: ${formId})`;
- submissionsSection.classList.remove("hidden");
-
- try {
- const submissions = await makeApiRequest(
- `/forms/${formId}/submissions`,
- "GET",
- null,
- true
- );
- submissionsList.innerHTML = ""; // Clear list
-
- if (submissions && submissions.length > 0) {
- submissions.forEach((sub) => {
- const li = document.createElement("li");
- // Display submission data safely - avoid rendering raw HTML
- const pre = document.createElement("pre");
- pre.textContent = JSON.stringify(sub.data, null, 2); // Pretty print JSON
- li.appendChild(pre);
- // Optionally display submission ID and timestamp if available
- // const info = document.createElement('small');
- // info.textContent = `ID: ${sub.id}, Submitted: ${sub.created_at || 'N/A'}`;
- // li.appendChild(info);
-
- submissionsList.appendChild(li);
- });
- } else {
- submissionsList.innerHTML =
- "No submissions found for this form. ";
- }
- } catch (error) {
- showStatus(
- `Failed to load submissions for form ${formId}: ${error.message}`,
- true
- );
- submissionsList.innerHTML = "Error loading submissions. ";
- submissionsSection.classList.add("hidden"); // Hide section on error
- }
- }
-
- // --- Public Form Handling ---
-
- if (loadPublicFormButton) {
- loadPublicFormButton.addEventListener("click", async () => {
- const formId = publicFormIdInput.value.trim();
- if (!formId) {
- showStatus("Please enter a Form ID.", true);
- return;
- }
- showStatus("");
- publicFormArea.classList.add("hidden");
- publicForm.innerHTML = "Loading form..."; // Clear previous form
-
- // NOTE: Fetching form definition is NOT directly possible with the current backend
- // The backend only provides GET /forms (all, protected) and GET /forms/{id}/submissions (protected)
- // It DOES NOT provide a public GET /forms/{id} endpoint to fetch the definition.
- //
- // **WORKAROUND:** We will *assume* the user knows the structure or we have it cached/predefined.
- // For this example, we'll fetch *all* forms (if logged in) and find it, OR fail if not logged in.
- // A *better* backend design would include a public GET /forms/{id} endpoint.
-
- try {
- // Attempt to get the form definition (requires login for this workaround)
- if (!authToken) {
- showStatus(
- "Loading public forms requires login in this demo version.",
- true
- );
- publicForm.innerHTML = ""; // Clear loading message
- return;
- }
- const forms = await makeApiRequest("/forms", "GET", null, true);
- const formDefinition = forms.find((f) => f.id === formId);
-
- if (!formDefinition) {
- throw new Error(`Form with ID ${formId} not found or access denied.`);
- }
-
- renderPublicForm(formDefinition);
- publicFormArea.classList.remove("hidden");
- } catch (error) {
- showStatus(`Failed to load form ${formId}: ${error.message}`, true);
- publicForm.innerHTML = ""; // Clear loading message
- publicFormArea.classList.add("hidden");
- }
- });
- }
-
- function renderPublicForm(formDefinition) {
- publicFormTitle.textContent = formDefinition.name;
- publicForm.innerHTML = ""; // Clear previous fields
- publicForm.dataset.formId = formDefinition.id; // Store form ID for submission
-
- if (!formDefinition.fields || !Array.isArray(formDefinition.fields)) {
- publicForm.innerHTML = "Error: Form definition is invalid.
";
- console.error("Invalid form fields definition:", formDefinition.fields);
- return;
- }
-
- formDefinition.fields.forEach((field) => {
- const div = document.createElement("div");
- const label = document.createElement("label");
- label.htmlFor = `field-${field.name}`;
- label.textContent = field.label || field.name; // Use label, fallback to name
- div.appendChild(label);
-
- let input;
- // Basic type handling - could be expanded
- switch (field.type) {
- case "textarea": // Allow explicit textarea type
- case "string":
- // Use textarea for string if maxLength suggests it might be long
- if (field.maxLength && field.maxLength > 100) {
- input = document.createElement("textarea");
- input.rows = 4; // Default rows
- } else {
- input = document.createElement("input");
- input.type = "text";
- }
- if (field.minLength) input.minLength = field.minLength;
- if (field.maxLength) input.maxLength = field.maxLength;
- break;
- case "email":
- input = document.createElement("input");
- input.type = "email";
- break;
- case "url":
- input = document.createElement("input");
- input.type = "url";
- break;
- case "number":
- input = document.createElement("input");
- input.type = "number";
- if (field.min !== undefined) input.min = field.min;
- if (field.max !== undefined) input.max = field.max;
- input.step = field.step || "any"; // Allow decimals by default
- break;
- case "boolean":
- input = document.createElement("input");
- input.type = "checkbox";
- // Checkbox label handling is slightly different
- label.insertBefore(input, label.firstChild); // Put checkbox before text
- input.style.width = "auto"; // Override default width
- input.style.marginRight = "10px";
- break;
- // Add cases for 'select', 'radio', 'date' etc. if needed
- default:
- input = document.createElement("input");
- input.type = "text";
- console.warn(
- `Unsupported field type "${field.type}" for field "${field.name}". Rendering as text.`
- );
- }
-
- if (input.type !== "checkbox") {
- // Checkbox is already appended inside label
- div.appendChild(input);
- }
- input.id = `field-${field.name}`;
- input.name = field.name; // Crucial for form data collection
- if (field.required) input.required = true;
- if (field.placeholder) input.placeholder = field.placeholder;
- if (field.pattern) input.pattern = field.pattern; // Add regex pattern validation
-
- publicForm.appendChild(div);
- });
-
- const submitButton = document.createElement("button");
- submitButton.type = "submit";
- submitButton.textContent = "Submit Form";
- publicForm.appendChild(submitButton);
- }
-
- publicForm.addEventListener("submit", async (e) => {
- e.preventDefault();
- showStatus("");
- const formId = e.target.dataset.formId;
- if (!formId) {
- showStatus("Error: Form ID is missing.", true);
- return;
- }
-
- const formData = new FormData(e.target);
- const submissionData = {};
-
- // Convert FormData to a plain object, handling checkboxes correctly
- for (const [key, value] of formData.entries()) {
- const inputElement = e.target.elements[key];
-
- // Handle Checkboxes (boolean)
- if (inputElement && inputElement.type === "checkbox") {
- // A checkbox value is only present in FormData if it's checked.
- // We need to ensure we always send a boolean.
- // Check if the element exists in the form (it might be unchecked)
- submissionData[key] = inputElement.checked;
- }
- // Handle Number inputs (convert from string)
- else if (inputElement && inputElement.type === "number") {
- // Only convert if the value is not empty, otherwise send null or handle as needed
- if (value !== "") {
- submissionData[key] = parseFloat(value); // Or parseInt if only integers allowed
- if (isNaN(submissionData[key])) {
- // Handle potential parsing errors if input validation fails
- console.warn(`Could not parse number for field ${key}: ${value}`);
- submissionData[key] = null; // Or keep as string, or show error
- }
- } else {
- submissionData[key] = null; // Or undefined, depending on backend expectation for empty numbers
- }
- }
- // Handle potential multiple values for the same name (e.g., multi-select), though not rendered here
- else if (submissionData.hasOwnProperty(key)) {
- if (!Array.isArray(submissionData[key])) {
- submissionData[key] = [submissionData[key]];
- }
- submissionData[key].push(value);
- }
- // Default: treat as string
- else {
- submissionData[key] = value;
- }
- }
-
- // Ensure boolean fields that were *unchecked* are explicitly set to false
- // FormData only includes checked checkboxes. Find all checkbox inputs in the form.
- const checkboxes = e.target.querySelectorAll('input[type="checkbox"]');
- checkboxes.forEach((cb) => {
- if (!submissionData.hasOwnProperty(cb.name)) {
- submissionData[cb.name] = false; // Set unchecked boxes to false
- }
- });
-
- console.log("Submitting data:", submissionData); // Debugging
-
- try {
- // Public submission endpoint doesn't require auth
- const result = await makeApiRequest(
- `/forms/${formId}/submissions`,
- "POST",
- submissionData,
- false
- );
- showStatus(
- `Submission successful! Submission ID: ${result.submission_id}`
- );
- e.target.reset(); // Clear the form
- // Optionally hide the form after successful submission
- // publicFormArea.classList.add('hidden');
- } catch (error) {
- let errorMsg = `Submission failed: ${error.message}`;
- // Handle validation errors specifically
- if (error.validationErrors) {
- errorMsg = "Submission failed due to validation errors:\n";
- for (const [field, message] of Object.entries(error.validationErrors)) {
- errorMsg += `- ${field}: ${message}\n`;
- }
- // Highlight invalid fields? (More complex UI update)
- }
- showStatus(errorMsg, true);
- }
- });
-
- // --- Initial Setup ---
- toggleSections(); // Set initial view based on stored token
- if (authToken) {
- loadFormsButton.click(); // Auto-load forms if logged in
- }
-});
diff --git a/frontend/style.css b/frontend/style.css
deleted file mode 100644
index 33e22c2..0000000
--- a/frontend/style.css
+++ /dev/null
@@ -1,411 +0,0 @@
-/* --- Variables copied from FormCraft --- */
-:root {
- --color-bg: #f7f7f7;
- --color-surface: #ffffff;
- --color-primary: #3a4750; /* Dark grayish blue */
- --color-secondary: #d8d8d8; /* Light gray */
- --color-accent: #b06f42; /* Warm wood/leather brown */
- --color-text: #2d3436; /* Dark gray */
- --color-text-light: #636e72; /* Medium gray */
- --color-border: #e0e0e0; /* Light border gray */
- --color-success: #2e7d32; /* Green */
- --color-success-bg: #e8f5e9;
- --color-error: #a94442; /* Red for errors */
- --color-error-bg: #f2dede;
- --color-danger: #e74c3c; /* Red for danger buttons */
- --color-danger-hover: #c0392b;
-
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
- --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05);
- --border-radius: 6px;
-}
-
-/* --- Global Reset & Body Styles --- */
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
-}
-
-body {
- background-color: var(--color-bg);
- color: var(--color-text);
- line-height: 1.6;
- min-height: 100vh;
- display: flex; /* Helps with potential footer later */
- flex-direction: column;
-}
-
-/* --- Container --- */
-.container {
- max-width: 900px; /* Adjusted width for simpler content */
- width: 100%;
- margin: 0 auto;
- padding: 32px 24px; /* Add padding like main content */
-}
-
-.page-container {
- flex: 1; /* Make container take available space if using flex on body */
-}
-
-/* --- Typography --- */
-h1,
-h2,
-h3 {
- color: var(--color-primary);
- margin-bottom: 16px;
- line-height: 1.3;
-}
-
-h1.page-title {
- font-size: 1.75rem;
- font-weight: 600;
- margin-bottom: 24px;
- text-align: center; /* Center main title */
-}
-
-h2.section-title {
- font-size: 1.25rem;
- font-weight: 600;
- border-bottom: 1px solid var(--color-border);
- padding-bottom: 8px;
- margin-bottom: 20px;
-}
-
-h3.card-title {
- font-size: 1.1rem;
- font-weight: 600;
- color: var(--color-primary);
- margin-bottom: 16px;
-}
-
-p {
- margin-bottom: 16px;
- color: var(--color-text-light);
-}
-p:last-child {
- margin-bottom: 0;
-}
-
-hr.divider {
- border: 0;
- height: 1px;
- background: var(--color-border);
- margin: 32px 0;
-}
-
-/* --- Content Card / Section Styling --- */
-.content-card,
-.section {
- background-color: var(--color-surface);
- padding: 24px;
- margin-bottom: 24px;
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius);
- box-shadow: var(--shadow-sm);
-}
-
-.admin-header p {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 0;
- color: var(--color-text);
- font-weight: 500;
-}
-
-.admin-header span {
- font-weight: 600;
- color: var(--color-primary);
-}
-
-/* --- Forms --- */
-form .form-group {
- margin-bottom: 16px;
-}
-/* For side-by-side input and button */
-form .inline-form-group {
- display: flex;
- gap: 10px;
- align-items: flex-start; /* Align items to top */
-}
-form .inline-form-group input {
- flex-grow: 1; /* Allow input to take available space */
- margin-bottom: 0; /* Remove bottom margin */
-}
-form .inline-form-group button {
- flex-shrink: 0; /* Prevent button from shrinking */
-}
-
-label {
- display: block;
- margin-bottom: 6px;
- font-weight: 500;
- font-size: 0.9rem;
- color: var(--color-text-light);
-}
-
-input[type="text"],
-input[type="password"],
-input[type="email"],
-input[type="url"],
-input[type="number"],
-textarea {
- width: 100%;
- padding: 10px 12px;
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius);
- font-size: 0.95rem;
- color: var(--color-text);
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
-}
-
-input[type="text"]:focus,
-input[type="password"]:focus,
-input[type="email"]:focus,
-input[type="url"]:focus,
-input[type="number"]:focus,
-textarea:focus {
- outline: none;
- border-color: var(--color-accent);
- box-shadow: 0 0 0 2px rgba(176, 111, 66, 0.2); /* Accent focus ring */
-}
-
-textarea {
- min-height: 80px;
- resize: vertical;
-}
-
-/* Styling for dynamically generated public form fields */
-#public-form div {
- margin-bottom: 16px; /* Keep consistent spacing */
-}
-
-/* Specific styles for checkboxes */
-#public-form input[type="checkbox"] {
- width: auto; /* Override 100% width */
- margin-right: 10px;
- vertical-align: middle; /* Align checkbox nicely with label text */
- margin-bottom: 0; /* Remove bottom margin if label handles spacing */
-}
-#public-form input[type="checkbox"] + label, /* Style label differently if needed when next to checkbox */
-#public-form label input[type="checkbox"] /* Style if checkbox is inside label */ {
- display: inline-flex; /* Or inline-block */
- align-items: center;
- margin-bottom: 0; /* Prevent double margin */
- font-weight: normal; /* Checkboxes often have normal weight labels */
- color: var(--color-text);
-}
-
-/* --- Buttons --- */
-.button {
- background-color: var(--color-primary);
- color: white;
- border: 1px solid transparent; /* Add border for consistency */
- padding: 10px 18px;
- border-radius: var(--border-radius);
- font-weight: 500;
- font-size: 0.9rem;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- transition: all 0.2s ease;
- text-decoration: none;
- line-height: 1.5;
- vertical-align: middle; /* Align with text/inputs */
-}
-
-.button:hover {
- background-color: #2c373f; /* Slightly darker hover */
- box-shadow: var(--shadow-sm);
-}
-.button:active {
- background-color: #1e2a31; /* Even darker active state */
-}
-
-.button-secondary {
- background-color: var(--color-surface);
- color: var(--color-primary);
- border: 1px solid var(--color-border);
-}
-
-.button-secondary:hover {
- background-color: #f8f8f8; /* Subtle hover for secondary */
- border-color: #d0d0d0;
-}
-.button-secondary:active {
- background-color: #f0f0f0;
-}
-
-.button-danger {
- background-color: var(--color-danger);
- border-color: var(--color-danger);
-}
-.button-danger:hover {
- background-color: var(--color-danger-hover);
- border-color: var(--color-danger-hover);
-}
-.button-danger:active {
- background-color: #a52e22; /* Even darker red */
-}
-
-/* Smaller button variant for lists? */
-.button-sm {
- padding: 5px 10px;
- font-size: 0.8rem;
-}
-
-/* Ensure buttons added by JS (like submit in public form) get styled */
-#public-form button[type="submit"] {
- /* Inherit .button styles if possible, otherwise redefine */
- background-color: var(--color-primary);
- color: white;
- border: 1px solid transparent;
- padding: 10px 18px;
- border-radius: var(--border-radius);
- font-weight: 500;
- font-size: 0.9rem;
- cursor: pointer;
- transition: all 0.2s ease;
- line-height: 1.5;
- margin-top: 10px; /* Add some space above submit */
-}
-#public-form button[type="submit"]:hover {
- background-color: #2c373f;
- box-shadow: var(--shadow-sm);
-}
-#public-form button[type="submit"]:active {
- background-color: #1e2a31;
-}
-
-/* --- Lists (Forms & Submissions) --- */
-ul.styled-list {
- list-style: none;
- padding: 0;
- margin-top: 20px; /* Space below heading/button */
-}
-
-ul.styled-list li {
- background-color: #fcfcfc; /* Slightly off-white */
- border: 1px solid var(--color-border);
- padding: 12px 16px;
- margin-bottom: 8px;
- border-radius: var(--border-radius);
- display: flex;
- justify-content: space-between;
- align-items: center;
- transition: background-color 0.2s ease;
- font-size: 0.95rem;
-}
-
-ul.styled-list li:hover {
- background-color: #f5f5f5;
-}
-
-ul.styled-list li button {
- margin-left: 16px; /* Space between text and button */
- /* Use smaller button style */
- padding: 5px 10px;
- font-size: 0.8rem;
- /* Inherit base button colors or use secondary */
- background-color: var(--color-surface);
- color: var(--color-primary);
- border: 1px solid var(--color-border);
-}
-ul.styled-list li button:hover {
- background-color: #f8f8f8;
- border-color: #d0d0d0;
-}
-
-/* Specific styling for submissions list items */
-ul.submissions li {
- display: block; /* Allow pre tag to format */
- background-color: var(--color-surface); /* White background for submissions */
-}
-
-ul.submissions li pre {
- white-space: pre-wrap; /* Wrap long lines */
- word-wrap: break-word; /* Break long words */
- background-color: #f9f9f9; /* Light grey background for code block */
- padding: 10px;
- border-radius: var(--border-radius);
- border: 1px solid var(--color-border);
- font-size: 0.85rem;
- color: var(--color-text);
- max-height: 200px; /* Limit height */
- overflow-y: auto; /* Add scroll if needed */
-}
-
-/* --- Status Area --- */
-.status {
- padding: 12px 16px;
- margin-bottom: 20px;
- border-radius: var(--border-radius);
- font-weight: 500;
- border: 1px solid transparent;
- display: none; /* Hide by default, JS shows it */
-}
-.status.success,
-.status.error {
- display: block; /* Show when class is added */
-}
-
-.status.success {
- background-color: var(--color-success-bg);
- color: var(--color-success);
- border-color: var(--color-success); /* Darker green border */
-}
-.status.error {
- background-color: var(--color-error-bg);
- color: var(--color-error);
- border-color: var(--color-error); /* Darker red border */
- white-space: pre-wrap; /* Allow multi-line errors */
-}
-
-/* --- Utility --- */
-.hidden {
- display: none !important; /* Use !important to override potential inline styles if needed */
-}
-
-/* --- Responsive Adjustments (Basic) --- */
-@media (max-width: 768px) {
- .container {
- padding: 24px 16px;
- }
- h1.page-title {
- font-size: 1.5rem;
- }
- h2.section-title {
- font-size: 1.15rem;
- }
- ul.styled-list li {
- flex-direction: column;
- align-items: flex-start;
- gap: 8px;
- }
- ul.styled-list li button {
- margin-left: 0;
- align-self: flex-end; /* Move button to bottom right */
- }
- form .inline-form-group {
- flex-direction: column;
- align-items: stretch; /* Make elements full width */
- }
- form .inline-form-group button {
- width: 100%; /* Make button full width */
- }
-}
-
-@media (max-width: 576px) {
- .content-card,
- .section {
- padding: 16px;
- }
- .button {
- padding: 8px 14px;
- font-size: 0.85rem;
- }
-}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1a39a33
--- /dev/null
+++ b/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "formies",
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "basic-auth": "^2.0.1",
+ "cors": "^2.8.5",
+ "dotenv": "^16.5.0",
+ "ejs": "^3.1.10",
+ "express": "^5.1.0",
+ "helmet": "^8.1.0",
+ "node-fetch": "^3.3.2",
+ "sqlite": "^5.1.1",
+ "sqlite3": "^5.1.7",
+ "uuid": "^11.1.0"
+ }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..31eebc7
--- /dev/null
+++ b/server.js
@@ -0,0 +1,53 @@
+import dotenv from "dotenv";
+import express from "express";
+import helmet from "helmet";
+import cors from "cors";
+import adminRoutes from "./src/routes/admin.js";
+import publicRoutes from "./src/routes/public.js";
+
+dotenv.config();
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+
+// CORS configuration
+app.use(cors({
+ origin: ['https://mohamad.dev', 'https://www.mohamad.dev'],
+ methods: ['GET', 'POST'],
+ credentials: true
+}));
+
+// Middleware
+app.use(helmet({
+ crossOriginResourcePolicy: { policy: "cross-origin" }
+}));
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+app.set("view engine", "ejs");
+app.use(express.static('views', {
+ setHeaders: (res, path) => {
+ if (path.endsWith('.js')) {
+ res.setHeader('Content-Type', 'application/javascript');
+ }
+ }
+}));
+
+// Routes
+app.use("/admin", adminRoutes);
+app.use("/", publicRoutes);
+
+// Start server
+app.listen(80, () => {
+ console.log(`Server running on http://0.0.0.0:${PORT}`);
+ if (!process.env.ADMIN_USER || !process.env.ADMIN_PASSWORD) {
+ console.warn("WARNING: Admin routes are UNPROTECTED. Set ADMIN_USER and ADMIN_PASSWORD in .env");
+ }
+ if (process.env.ADMIN_USER && process.env.ADMIN_PASSWORD) {
+ console.log(`Admin access: User: ${process.env.ADMIN_USER}, Pass: (hidden)`);
+ }
+ if (process.env.NTFY_ENABLED === "true" && process.env.NTFY_TOPIC_URL) {
+ console.log(`Ntfy notifications enabled for topic: ${process.env.NTFY_TOPIC_URL}`);
+ } else {
+ console.log("Ntfy notifications disabled or topic not configured.");
+ }
+});
diff --git a/src/auth.rs b/src/auth.rs
deleted file mode 100644
index 75b7620..0000000
--- a/src/auth.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-// src/auth.rs
-use super::AppState;
-use actix_web::error::{ErrorInternalServerError, ErrorUnauthorized}; // Specific error types
-use actix_web::{
- dev::Payload, http::header::AUTHORIZATION, web, Error as ActixWebError, FromRequest,
- HttpRequest,
-};
-use futures::future::{ready, Ready};
-use log; // Use the log crate
-use rusqlite::Connection;
-use std::sync::{Arc, Mutex}; // Import AppState from the parent module (main.rs likely)
-
-// Represents an authenticated user via token
-pub struct Auth {
- pub user_id: String,
-}
-
-impl FromRequest for Auth {
- // Use actix_web::Error for consistency in error handling within Actix
- type Error = ActixWebError;
- // Use Ready from futures 0.3
- type Future = Ready>;
-
- fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
- // Extract database connection pool from application data
- // Extract the *whole* AppState first
- let app_state_result = req.app_data::>();
-
- // Get the Arc> from AppState
- let db_arc_mutex = match app_state_result {
- // Access the 'db' field within the AppState
- Some(data) => data.db.clone(), // Clone the Arc, not the Mutex or Connection
- None => {
- log::error!("Database connection missing in application data configuration.");
- return ready(Err(ErrorInternalServerError(
- "Internal server error (app configuration)",
- )));
- }
- };
-
- // Extract Authorization header
- let auth_header = req.headers().get(AUTHORIZATION);
-
- if let Some(auth_header_value) = auth_header {
- // Convert header value to string
- if let Ok(auth_str) = auth_header_value.to_str() {
- // Check if it starts with "Bearer "
- if auth_str.starts_with("Bearer ") {
- // Extract the token part
- let token = &auth_str[7..];
-
- // Lock the mutex to get access to the connection
- // Handle potential mutex poisoning explicitly
- let conn_guard = match db_arc_mutex.lock() {
- Ok(guard) => guard,
- Err(poisoned) => {
- log::error!("Database mutex poisoned: {}", poisoned);
- // Return internal server error if mutex is poisoned
- return ready(Err(ErrorInternalServerError(
- "Internal server error (database lock)",
- )));
- }
- };
-
- // Validate the token against the database (now includes expiration check)
- match super::db::validate_token(&conn_guard, token) {
- // Token is valid and not expired, return Ok with Auth struct
- Ok(Some(user_id)) => {
- log::debug!("Token validated successfully for user_id: {}", user_id);
- ready(Ok(Auth { user_id }))
- }
- // Token is invalid, not found, or expired
- Ok(None) => {
- log::warn!("Invalid or expired token received"); // Avoid logging token
- ready(Err(ErrorUnauthorized("Invalid or expired token")))
- }
- // Database error during token validation
- Err(e) => {
- log::error!("Database error during token validation: {:?}", e);
- // Return Unauthorized to avoid leaking internal error details
- // Consider mapping specific DB errors if needed, but Unauthorized is generally safe
- ready(Err(ErrorUnauthorized("Token validation failed")))
- }
- }
- } else {
- // Header present but not "Bearer " format
- log::warn!("Invalid Authorization header format (not Bearer)");
- ready(Err(ErrorUnauthorized("Invalid token format")))
- }
- } else {
- // Header value contains invalid characters
- log::warn!("Authorization header contains invalid characters");
- ready(Err(ErrorUnauthorized("Invalid token value")))
- }
- } else {
- // Authorization header is missing
- log::warn!("Missing Authorization header");
- ready(Err(ErrorUnauthorized("Missing authorization token")))
- }
- }
-}
diff --git a/src/config/database.js b/src/config/database.js
new file mode 100644
index 0000000..6782f16
--- /dev/null
+++ b/src/config/database.js
@@ -0,0 +1,34 @@
+import sqlite3 from 'sqlite3';
+import { open } from 'sqlite';
+
+// Create a database connection
+const db = await open({
+ filename: './database.sqlite',
+ driver: sqlite3.Database
+});
+
+// Initialize tables if they don't exist
+await db.exec(`
+ CREATE TABLE IF NOT EXISTS forms (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ uuid TEXT NOT NULL UNIQUE,
+ name TEXT DEFAULT 'My Form',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ thank_you_url TEXT,
+ thank_you_message TEXT,
+ ntfy_enabled INTEGER DEFAULT 1,
+ is_archived INTEGER DEFAULT 0,
+ allowed_domains TEXT
+ );
+
+ CREATE TABLE IF NOT EXISTS submissions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ form_uuid TEXT NOT NULL,
+ data TEXT NOT NULL,
+ ip_address TEXT,
+ submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (form_uuid) REFERENCES forms(uuid) ON DELETE CASCADE
+ );
+`);
+
+export default db;
diff --git a/src/db.rs b/src/db.rs
deleted file mode 100644
index 2a52f67..0000000
--- a/src/db.rs
+++ /dev/null
@@ -1,356 +0,0 @@
-// src/db.rs
-use anyhow::{anyhow, Context, Result as AnyhowResult};
-use bcrypt::{hash, verify, DEFAULT_COST};
-use chrono::{Duration as ChronoDuration, Utc}; // Use Utc for timestamps
-use log; // Use the log crate
-use rusqlite::{params, Connection, OptionalExtension};
-use std::env;
-use uuid::Uuid;
-
-use crate::models;
-
-// Configurable token lifetime (e.g., from environment variable or default)
-const TOKEN_LIFETIME_HOURS: i64 = 24; // Default to 24 hours
-
-// Initialize the database connection and create tables if they don't exist
-pub fn init_db(database_url: &str) -> AnyhowResult {
- log::info!("Attempting to open or create database at: {}", database_url);
- let conn = Connection::open(database_url)
- .context(format!("Failed to open the database at {}", database_url))?;
-
- log::debug!("Creating 'users' table if not exists...");
- conn.execute(
- "CREATE TABLE IF NOT EXISTS users (
- id TEXT PRIMARY KEY,
- username TEXT NOT NULL UNIQUE,
- password TEXT NOT NULL, -- Stores bcrypt hashed password
- token TEXT UNIQUE, -- Stores the current session token (UUID)
- token_expires_at DATETIME -- Timestamp when the token expires
- )",
- [],
- )
- .context("Failed to create 'users' table")?;
-
- log::debug!("Creating 'forms' table if not exists...");
- conn.execute(
- "CREATE TABLE IF NOT EXISTS forms (
- id TEXT PRIMARY KEY,
- name TEXT NOT NULL,
- fields TEXT NOT NULL, -- Stores JSON definition of form fields
- notify_email TEXT, -- Optional email address for notifications
- notify_ntfy_topic TEXT, -- Optional ntfy topic for notifications
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
- )",
- [],
- )
- .context("Failed to create 'forms' table")?;
-
- // Add notify_email column if it doesn't exist (for backward compatibility)
- match conn.execute("ALTER TABLE forms ADD COLUMN notify_email TEXT", []) {
- Ok(_) => log::info!("Added notify_email column to forms table"),
- Err(e) => {
- if !e.to_string().contains("duplicate column name") {
- return Err(anyhow!("Failed to add notify_email column: {}", e));
- }
- // If it already exists, that's fine
- }
- }
-
- // Add notify_ntfy_topic column if it doesn't exist (for backward compatibility)
- match conn.execute("ALTER TABLE forms ADD COLUMN notify_ntfy_topic TEXT", []) {
- Ok(_) => log::info!("Added notify_ntfy_topic column to forms table"),
- Err(e) => {
- if !e.to_string().contains("duplicate column name") {
- return Err(anyhow!("Failed to add notify_ntfy_topic column: {}", e));
- }
- // If it already exists, that's fine
- }
- }
-
- log::debug!("Creating 'submissions' table if not exists...");
- conn.execute(
- "CREATE TABLE IF NOT EXISTS submissions (
- id TEXT PRIMARY KEY,
- form_id TEXT NOT NULL,
- data TEXT NOT NULL, -- Stores JSON submission data
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (form_id) REFERENCES forms (id) ON DELETE CASCADE
- )",
- [],
- )
- .context("Failed to create 'submissions' table")?;
-
- // Setup the initial admin user if it doesn't exist, using environment variables
- setup_initial_admin(&conn).context("Failed to setup initial admin user")?;
-
- log::info!("Database initialization complete.");
- Ok(conn)
-}
-
-// Sets up the initial admin user from *required* environment variables if it doesn't exist
-fn setup_initial_admin(conn: &Connection) -> AnyhowResult<()> {
- // CRITICAL SECURITY CHANGE: Remove default credentials. Require env vars.
- let initial_admin_username = env::var("INITIAL_ADMIN_USERNAME")
- .map_err(|_| anyhow!("FATAL: INITIAL_ADMIN_USERNAME environment variable is not set."))?;
- let initial_admin_password = env::var("INITIAL_ADMIN_PASSWORD")
- .map_err(|_| anyhow!("FATAL: INITIAL_ADMIN_PASSWORD environment variable is not set."))?;
-
- if initial_admin_username.is_empty() || initial_admin_password.is_empty() {
- return Err(anyhow!(
- "FATAL: INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD must not be empty."
- ));
- }
-
- // Check password complexity? (Optional enhancement)
-
- add_user_if_not_exists(conn, &initial_admin_username, &initial_admin_password)
- .context("Failed during initial admin user setup")?;
- Ok(())
-}
-
-// Adds a user with a hashed password if the username doesn't exist
-pub fn add_user_if_not_exists(
- conn: &Connection,
- username: &str,
- password: &str,
-) -> AnyhowResult {
- // Check if user already exists
- let user_exists: bool = conn
- .query_row(
- "SELECT EXISTS(SELECT 1 FROM users WHERE username = ?1)",
- params![username],
- |row| row.get::<_, i32>(0),
- )
- .context(format!("Failed to check existence of user '{}'", username))?
- == 1;
-
- if user_exists {
- log::debug!("User '{}' already exists, skipping creation.", username);
- return Ok(false); // User already exists, nothing added
- }
-
- // Generate a UUID for the new user
- let user_id = Uuid::new_v4().to_string();
-
- // Hash the password using bcrypt
- // Ensure the cost factor is appropriate for your security needs and hardware.
- // Higher cost means slower hashing and verification, but better resistance to brute-force.
- log::debug!(
- "Hashing password for user '{}' with cost {}",
- username,
- DEFAULT_COST
- );
- let hashed_password = hash(password, DEFAULT_COST).context("Failed to hash password")?;
-
- // Insert the new user (token and expiry are initially NULL)
- log::info!("Creating new user '{}' with ID: {}", username, user_id);
- conn.execute(
- "INSERT INTO users (id, username, password) VALUES (?1, ?2, ?3)",
- params![user_id, username, hashed_password],
- )
- .context(format!("Failed to insert user '{}'", username))?;
-
- Ok(true) // User was added
-}
-
-// Validate a session token and return the associated user ID if valid and not expired
-pub fn validate_token(conn: &Connection, token: &str) -> AnyhowResult> {
- log::debug!("Validating received token (existence and expiration)...");
- let mut stmt = conn.prepare(
- // Select user ID only if token matches AND it hasn't expired
- "SELECT id FROM users WHERE token = ?1 AND token_expires_at IS NOT NULL AND token_expires_at > ?2"
- ).context("Failed to prepare query for validating token")?;
-
- let now_ts = Utc::now().to_rfc3339(); // Use ISO 8601 / RFC 3339 format compatible with SQLite DATETIME
-
- let user_id_option: Option = stmt
- .query_row(params![token, now_ts], |row| row.get(0))
- .optional() // Makes it return Option instead of erroring on no rows
- .context("Failed to execute query for validating token")?;
-
- if user_id_option.is_some() {
- log::debug!("Token validation successful.");
- } else {
- // This covers token not found OR token expired
- log::debug!("Token validation failed (token not found or expired).");
- }
-
- Ok(user_id_option)
-}
-
-// Invalidate a user's token (e.g., on logout) by setting it to NULL and clearing expiration
-pub fn invalidate_token(conn: &Connection, user_id: &str) -> AnyhowResult<()> {
- log::debug!("Invalidating token for user_id {}", user_id);
- conn.execute(
- "UPDATE users SET token = NULL, token_expires_at = NULL WHERE id = ?1",
- params![user_id],
- )
- .context(format!(
- "Failed to invalidate token for user_id {}",
- user_id
- ))?;
- Ok(())
-}
-
-// Authenticate a user by username and password, returning user ID and hash if successful
-pub fn authenticate_user(
- conn: &Connection,
- username: &str,
- password: &str,
-) -> AnyhowResult> {
- log::debug!("Attempting to authenticate user: {}", username);
- let mut stmt = conn
- .prepare("SELECT id, password FROM users WHERE username = ?1")
- .context("Failed to prepare query for authenticating user")?;
-
- let result = stmt
- .query_row(params![username], |row| {
- Ok(models::UserAuthData {
- id: row.get(0)?,
- hashed_password: row.get(1)?,
- })
- })
- .optional()
- .context(format!(
- "Failed to execute query to fetch auth data for user '{}'",
- username
- ))?;
-
- match result {
- Some(user_data) => {
- // Verify the provided password against the stored hash
- let is_valid = verify(password, &user_data.hashed_password)
- .context("Failed to verify password hash")?;
-
- if is_valid {
- log::info!("Authentication successful for user: {}", username);
- Ok(Some(user_data)) // Return user ID and hash
- } else {
- log::warn!(
- "Authentication failed for user '{}' (invalid password)",
- username
- );
- Ok(None) // Invalid password
- }
- }
- None => {
- log::warn!(
- "Authentication failed for user '{}' (user not found)",
- username
- );
- Ok(None) // User not found
- }
- }
-}
-
-// Generate and save a new session token (with expiration) for a user
-pub fn generate_and_set_token_for_user(conn: &Connection, user_id: &str) -> AnyhowResult {
- let new_token = Uuid::new_v4().to_string();
- // Calculate expiration time
- let expires_at = Utc::now() + ChronoDuration::hours(TOKEN_LIFETIME_HOURS);
- let expires_at_ts = expires_at.to_rfc3339(); // Store as string
-
- log::debug!(
- "Generating new token for user_id {} expiring at {}",
- user_id,
- expires_at_ts
- );
-
- conn.execute(
- "UPDATE users SET token = ?1, token_expires_at = ?2 WHERE id = ?3",
- params![new_token, expires_at_ts, user_id],
- )
- .context(format!("Failed to update token for user_id {}", user_id))?;
-
- Ok(new_token)
-}
-
-// Fetch a specific form definition by its ID
-pub fn get_form_definition(conn: &Connection, form_id: &str) -> AnyhowResult> {
- let mut stmt = conn
- .prepare("SELECT id, name, fields, notify_email, notify_ntfy_topic, created_at FROM forms WHERE id = ?1")
- .context("Failed to prepare query for fetching form")?;
-
- let result = stmt
- .query_row(params![form_id], |row| {
- let id: String = row.get(0)?;
- let name: String = row.get(1)?;
- let fields_str: String = row.get(2)?;
- let notify_email: Option = row.get(3)?;
- let notify_ntfy_topic: Option = row.get(4)?; // Get the new field
- let created_at: chrono::DateTime = row.get(5)?;
-
- // Parse the fields JSON string
- let fields = serde_json::from_str(&fields_str).map_err(|e| {
- rusqlite::Error::FromSqlConversionFailure(
- 2, // Index of 'fields' column
- rusqlite::types::Type::Text,
- Box::new(e),
- )
- })?;
-
- Ok(models::Form {
- id: Some(id),
- name,
- fields,
- notify_email,
- notify_ntfy_topic, // Include the new field
- created_at,
- })
- })
- .optional()
- .context(format!("Failed to fetch form with ID: {}", form_id))?;
-
- Ok(result)
-}
-
-// Add a function to save a form
-impl models::Form {
- pub fn save(&self, conn: &Connection) -> AnyhowResult<()> {
- let id = self
- .id
- .clone()
- .unwrap_or_else(|| Uuid::new_v4().to_string());
- let fields_json = serde_json::to_string(&self.fields)?;
-
- conn.execute(
- "INSERT INTO forms (id, name, fields, notify_email, notify_ntfy_topic, created_at)
- VALUES (?1, ?2, ?3, ?4, ?5, ?6)
- ON CONFLICT(id) DO UPDATE SET
- name = excluded.name,
- fields = excluded.fields,
- notify_email = excluded.notify_email,
- notify_ntfy_topic = excluded.notify_ntfy_topic", // Update the new field on conflict
- params![
- id,
- self.name,
- fields_json,
- self.notify_email,
- self.notify_ntfy_topic, // Add the new field to params
- self.created_at
- ],
- )?;
-
- Ok(())
- }
-
- pub fn get_by_id(conn: &Connection, id: &str) -> AnyhowResult {
- get_form_definition(conn, id)?.ok_or_else(|| anyhow!("Form not found: {}", id))
- // Added ID to error
- }
-}
-
-// Add a function to save a submission
-impl models::Submission {
- pub fn save(&self, conn: &Connection) -> AnyhowResult<()> {
- let data_json = serde_json::to_string(&self.data)?;
-
- conn.execute(
- "INSERT INTO submissions (id, form_id, data, created_at)
- VALUES (?1, ?2, ?3, ?4)",
- params![self.id, self.form_id, data_json, self.created_at],
- )?;
-
- Ok(())
- }
-}
diff --git a/src/handlers.rs b/src/handlers.rs
deleted file mode 100644
index 2a00411..0000000
--- a/src/handlers.rs
+++ /dev/null
@@ -1,751 +0,0 @@
-use crate::auth::Auth;
-use crate::models::{Form, LoginCredentials, LoginResponse, Submission};
-use crate::AppState;
-use actix_web::{web, Error as ActixWebError, HttpResponse, Responder, Result as ActixResult};
-use chrono; // Only import the module since we use it qualified
-use log;
-use regex::Regex; // For pattern validation
-use rusqlite::{params, Connection};
-use serde_json::{json, Map, Value as JsonValue}; // Alias for clarity
-use std::collections::HashMap;
-use std::sync::{Arc, Mutex};
-use uuid::Uuid;
-
-// --- Helper Function for Validation ---
-
-/// Validates submission data against the form field definitions with enhanced checks.
-///
-/// Expected field definition properties:
-/// - `name`: string (required)
-/// - `type`: string (e.g., "string", "number", "boolean", "email", "url", "object", "array") (required)
-/// - `required`: boolean (optional, default: false)
-/// - `maxLength`: number (for "string" type)
-/// - `minLength`: number (for "string" type)
-/// - `min`: number (for "number" type)
-/// - `max`: number (for "number" type)
-/// - `pattern`: string (regex for "string", "email", "url" types)
-///
-/// Returns `Ok(())` if valid, or `Err(JsonValue)` containing validation errors.
-fn validate_submission_against_definition(
- submission_data: &JsonValue,
- form_definition_fields: &JsonValue,
-) -> Result<(), JsonValue> {
- let mut errors: HashMap = HashMap::new();
-
- // Ensure 'fields' in the definition is a JSON array
- let field_definitions = match form_definition_fields.as_array() {
- Some(defs) => defs,
- None => {
- log::error!(
- "Form definition 'fields' is not a JSON array. Def: {:?}",
- form_definition_fields
- );
- errors.insert(
- "_internal".to_string(),
- "Invalid form definition format (not an array)".to_string(),
- );
- return Err(json!({ "validation_errors": errors }));
- }
- };
-
- // Ensure the submission data is a JSON object
- let data_map = match submission_data.as_object() {
- Some(map) => map,
- None => {
- errors.insert(
- "_submission".to_string(),
- "Submission data must be a JSON object".to_string(),
- );
- return Err(json!({ "validation_errors": errors }));
- }
- };
-
- // Build a map of valid field names to their definitions from the definition for quick lookup
- let defined_field_names: HashMap> = field_definitions
- .iter()
- .filter_map(|val| val.as_object())
- .filter_map(|def| {
- def.get("name")
- .and_then(JsonValue::as_str)
- .map(|name| (name.to_string(), def))
- })
- .collect();
-
- // 1. Check for submitted fields that are NOT in the definition
- for submitted_key in data_map.keys() {
- if !defined_field_names.contains_key(submitted_key) {
- errors.insert(
- submitted_key.clone(),
- "Unexpected field submitted".to_string(),
- );
- }
- }
- // Exit early if unexpected fields were found
- if !errors.is_empty() {
- log::warn!("Submission validation failed: Unexpected fields submitted.");
- return Err(json!({ "validation_errors": errors }));
- }
-
- // 2. Iterate through each field definition and validate corresponding submitted data
- for (field_name, field_def) in &defined_field_names {
- // Extract properties using helper functions for clarity
- let field_type = field_def
- .get("type")
- .and_then(JsonValue::as_str)
- .unwrap_or("string"); // Default to "string" if type is missing or not a string
- let is_required = field_def
- .get("required")
- .and_then(JsonValue::as_bool)
- .unwrap_or(false); // Default to false if required is missing or not a boolean
- let min_length = field_def.get("minLength").and_then(JsonValue::as_u64);
- let max_length = field_def.get("maxLength").and_then(JsonValue::as_u64);
- let min_value = field_def.get("min").and_then(JsonValue::as_f64); // Use f64 for flexibility
- let max_value = field_def.get("max").and_then(JsonValue::as_f64);
- let pattern = field_def.get("pattern").and_then(JsonValue::as_str);
-
- match data_map.get(field_name) {
- Some(submitted_value) if !submitted_value.is_null() => {
- // Field is present and not null, perform type and constraint checks
- let mut type_error = None;
- let mut constraint_errors = vec![];
-
- match field_type {
- "string" | "email" | "url" => {
- if let Some(s) = submitted_value.as_str() {
- if let Some(min) = min_length {
- if (s.chars().count() as u64) < min {
- // Use chars().count() for UTF-8 correctness
- constraint_errors
- .push(format!("Must be at least {} characters long", min));
- }
- }
- if let Some(max) = max_length {
- if (s.chars().count() as u64) > max {
- constraint_errors.push(format!(
- "Must be no more than {} characters long",
- max
- ));
- }
- }
- if let Some(pat) = pattern {
- // Consider caching compiled Regex if performance is critical
- // and patterns are reused frequently across requests.
- match Regex::new(pat) {
- Ok(re) => {
- if !re.is_match(s) {
- constraint_errors.push(format!("Does not match required pattern"));
- }
- }
- Err(e) => log::warn!("Invalid regex pattern '{}' in form definition for field '{}': {}", pat, field_name, e), // Log regex compilation error
- }
- }
- // Specific checks for email/url
- if field_type == "email" {
- // Basic email regex (adjust for stricter needs or use a validation crate)
- // This regex is very basic and allows many technically invalid addresses.
- // Consider crates like `validator` for more robust validation.
- let email_regex =
- Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap(); // Safe unwrap for known valid regex
- if !email_regex.is_match(s) {
- constraint_errors
- .push("Must be a valid email address".to_string());
- }
- }
- if field_type == "url" {
- // Basic URL check (consider `url` crate for robustness)
- if url::Url::parse(s).is_err() {
- constraint_errors.push("Must be a valid URL".to_string());
- }
- }
- } else {
- type_error = Some(format!("Expected a string for '{}'", field_name));
- }
- }
- "number" => {
- // Use as_f64 for flexibility (handles integers and floats)
- if let Some(num) = submitted_value.as_f64() {
- if let Some(min) = min_value {
- if num < min {
- constraint_errors.push(format!("Must be at least {}", min));
- }
- }
- if let Some(max) = max_value {
- if num > max {
- constraint_errors.push(format!("Must be no more than {}", max));
- }
- }
- } else {
- type_error = Some(format!("Expected a number for '{}'", field_name));
- }
- }
- "boolean" => {
- if !submitted_value.is_boolean() {
- type_error = Some(format!(
- "Expected a boolean (true/false) for '{}'",
- field_name
- ));
- }
- }
- "object" => {
- if !submitted_value.is_object() {
- type_error =
- Some(format!("Expected a JSON object for '{}'", field_name));
- }
- // TODO: Could add deeper validation for object structure here if needed based on definition
- }
- "array" => {
- if !submitted_value.is_array() {
- type_error =
- Some(format!("Expected a JSON array for '{}'", field_name));
- }
- // TODO: Could add validation for array elements here if needed based on definition
- }
- _ => {
- // Log unsupported types during development/debugging if necessary
- log::trace!(
- "Unsupported field type '{}' encountered during validation for field '{}'. Treating as valid.",
- field_type,
- field_name
- );
- // Assume valid if type is not specifically handled or unknown
- }
- }
-
- // Record errors found for this field
- if let Some(err) = type_error {
- errors.insert(field_name.clone(), err);
- } else if !constraint_errors.is_empty() {
- // Combine multiple constraint errors if necessary
- errors.insert(field_name.clone(), constraint_errors.join("; "));
- }
- } // End check for present and non-null value
- Some(_) => {
- // Value is present but explicitly null (e.g., "fieldName": null)
- if is_required {
- errors.insert(
- field_name.clone(),
- "This field is required and cannot be null".to_string(),
- );
- }
- // Otherwise, null is considered a valid (empty) value for non-required fields
- }
- None => {
- // Field is missing entirely from the submission object
- if is_required {
- errors.insert(field_name.clone(), "This field is required".to_string());
- }
- // Missing is valid for non-required fields
- }
- } // End match data_map.get(field_name)
- } // End loop through field definitions
-
- // Check if any errors were collected
- if errors.is_empty() {
- Ok(()) // Validation passed
- } else {
- log::info!(
- "Submission validation failed with {} error(s).", // Log only the count for brevity
- errors.len()
- );
- // Return a JSON object containing the specific validation errors
- Err(json!({ "validation_errors": errors }))
- }
-}
-
-// Helper function to convert anyhow::Error to actix_web::Error
-fn anyhow_to_actix_error(e: anyhow::Error) -> ActixWebError {
- actix_web::error::ErrorInternalServerError(e.to_string())
-}
-
-// --- Public Handlers ---
-
-// POST /login
-pub async fn login(
- app_state: web::Data, // Expect AppState like other handlers
- creds: web::Json,
-) -> ActixResult {
- // Clone the Arc> from AppState
- let db_conn_arc = app_state.db.clone();
- let username = creds.username.clone();
- let password = creds.password.clone();
-
- // Wrap the blocking database operations in web::block
- let auth_result = web::block(move || {
- // Use the cloned Arc here
- let conn = db_conn_arc
- .lock()
- .map_err(|_| anyhow::anyhow!("Database mutex poisoned during login lock"))?;
- crate::db::authenticate_user(&conn, &username, &password)
- })
- .await
- .map_err(|e| {
- log::error!("web::block error during authentication: {:?}", e);
- actix_web::error::ErrorInternalServerError("Authentication process failed (blocking error)")
- })?
- .map_err(anyhow_to_actix_error)?;
-
- match auth_result {
- Some(user_data) => {
- // Clone Arc again for token generation, using the AppState db field
- let db_conn_token_arc = app_state.db.clone();
- let user_id = user_data.id.clone();
-
- // Generate and store a new token within web::block
- let token = web::block(move || {
- // Use the cloned Arc here
- let conn = db_conn_token_arc
- .lock()
- .map_err(|_| anyhow::anyhow!("Database mutex poisoned during token lock"))?;
- crate::db::generate_and_set_token_for_user(&conn, &user_id)
- })
- .await
- .map_err(|e| {
- log::error!("web::block error during token generation: {:?}", e);
- actix_web::error::ErrorInternalServerError(
- "Failed to complete login (token generation blocking error)",
- )
- })?
- .map_err(anyhow_to_actix_error)?;
-
- log::info!("Login successful for user_id: {}", user_data.id);
- Ok(HttpResponse::Ok().json(LoginResponse { token }))
- }
- None => {
- log::warn!("Login failed for username: {}", creds.username);
- // Return 401 Unauthorized for failed login attempts
- Err(actix_web::error::ErrorUnauthorized(
- "Invalid username or password",
- ))
- }
- }
-}
-
-// POST /logout
-pub async fn logout(
- app_state: web::Data, // Expect AppState
- auth: Auth, // Requires authentication (extracts user_id from token)
-) -> ActixResult {
- log::info!("User {} requesting logout", auth.user_id);
- let db_conn_arc = app_state.db.clone(); // Get db from AppState
- let user_id = auth.user_id.clone();
-
- // Invalidate the token in the database within web::block
- web::block(move || {
- let conn = db_conn_arc // Use the cloned Arc
- .lock()
- .map_err(|_| anyhow::anyhow!("Database mutex poisoned during logout lock"))?;
- crate::db::invalidate_token(&conn, &user_id)
- })
- .await
- .map_err(|e| {
- // Use the original auth.user_id here as user_id moved into the block
- log::error!(
- "web::block error during logout for user {}: {:?}",
- auth.user_id,
- e
- );
- actix_web::error::ErrorInternalServerError("Logout failed (blocking error)")
- })?
- .map_err(anyhow_to_actix_error)?;
-
- log::info!("User {} logged out successfully", auth.user_id);
- Ok(HttpResponse::Ok().json(json!({ "message": "Logged out successfully" })))
-}
-
-// POST /forms/{form_id}/submissions
-pub async fn submit_form(
- app_state: web::Data,
- path: web::Path, // Extracts form_id from path
- submission_payload: web::Json, // Expect arbitrary JSON payload
-) -> ActixResult {
- let form_id = path.into_inner();
- let conn = app_state.db.lock().map_err(|e| {
- log::error!("Failed to acquire database lock: {}", e);
- actix_web::error::ErrorInternalServerError("Database error")
- })?;
-
- // Get form definition
- let form = Form::get_by_id(&conn, &form_id).map_err(anyhow_to_actix_error)?;
-
- // Validate submission against form definition
- if let Err(validation_errors) =
- validate_submission_against_definition(&submission_payload, &form.fields)
- {
- return Ok(HttpResponse::BadRequest().json(validation_errors));
- }
-
- // Create submission record
- let submission = Submission {
- id: Uuid::new_v4().to_string(),
- form_id: form_id.clone(),
- data: submission_payload.into_inner(),
- created_at: chrono::Utc::now(),
- };
-
- // Save submission to database
- submission.save(&conn).map_err(|e| {
- log::error!("Failed to save submission: {}", e);
- actix_web::error::ErrorInternalServerError("Failed to save submission")
- })?;
-
- // Send notifications if configured
- if let Some(notify_email) = form.notify_email {
- let email_subject = format!("New submission for form: {}", form.name);
- let email_body = format!(
- "A new submission has been received for form '{}'.\n\nSubmission ID: {}\nTimestamp: {}\n\nData:\n{}",
- form.name,
- submission.id,
- submission.created_at,
- serde_json::to_string_pretty(&submission.data).unwrap_or_default()
- );
-
- if let Err(e) = app_state
- .notification_service
- .send_email(¬ify_email, &email_subject, &email_body)
- .await
- {
- log::warn!("Failed to send email notification: {}", e);
- }
-
- // Also send ntfy notification if configured (sends to the global topic)
- if let Some(topic_flag) = &form.notify_ntfy_topic {
- // Use field presence as a flag
- if !topic_flag.is_empty() {
- // Check if the flag string is non-empty
- let ntfy_title = format!("New submission for: {}", form.name);
- let ntfy_message = format!("Form: {}\nSubmission ID: {}", form.name, submission.id);
- if let Err(e) = app_state.notification_service.send_ntfy(
- &ntfy_title,
- &ntfy_message,
- Some(3), // Medium priority
- ) {
- log::warn!("Failed to send ntfy notification (global topic): {}", e);
- }
- }
- }
- }
-
- Ok(HttpResponse::Created().json(json!({
- "message": "Submission received",
- "submission_id": submission.id
- })))
-}
-
-// POST /forms
-pub async fn create_form(
- app_state: web::Data,
- _auth: Auth, // Authentication check via Auth extractor
- payload: web::Json,
-) -> ActixResult {
- let payload = payload.into_inner();
-
- // Extract form data from payload
- let name = payload["name"]
- .as_str()
- .ok_or_else(|| actix_web::error::ErrorBadRequest("Missing or invalid 'name' field"))?
- .to_string();
-
- let fields = payload["fields"].clone();
- if !fields.is_array() {
- return Err(actix_web::error::ErrorBadRequest(
- "'fields' must be a JSON array",
- ));
- }
-
- let notify_email = payload["notify_email"].as_str().map(|s| s.to_string());
- let notify_ntfy_topic = payload["notify_ntfy_topic"].as_str().map(|s| s.to_string());
-
- // Create new form
- let form = Form {
- id: None, // Will be generated during save
- name,
- fields,
- notify_email,
- notify_ntfy_topic,
- created_at: chrono::Utc::now(),
- };
-
- // Save the form
- let conn = app_state.db.lock().map_err(|e| {
- log::error!("Failed to acquire database lock: {}", e);
- actix_web::error::ErrorInternalServerError("Database error")
- })?;
-
- form.save(&conn).map_err(|e| {
- log::error!("Failed to save form: {}", e);
- actix_web::error::ErrorInternalServerError("Failed to save form")
- })?;
-
- Ok(HttpResponse::Created().json(form))
-}
-
-// GET /forms
-pub async fn get_forms(
- app_state: web::Data,
- auth: Auth, // Requires authentication
-) -> ActixResult {
- log::info!("User {} requesting list of forms", auth.user_id);
-
- let conn = app_state.db.lock().map_err(|e| {
- log::error!("Failed to acquire database lock: {}", e);
- actix_web::error::ErrorInternalServerError("Database error")
- })?;
-
- let mut stmt = conn
- .prepare("SELECT id, name, fields, notify_email, notify_ntfy_topic, created_at FROM forms")
- .map_err(|e| {
- log::error!("Failed to prepare statement: {}", e);
- actix_web::error::ErrorInternalServerError("Database error")
- })?;
-
- let forms_iter = stmt
- .query_map([], |row| {
- let id: String = row.get(0)?;
- let name: String = row.get(1)?;
- let fields_str: String = row.get(2)?;
- let notify_email: Option = row.get(3)?;
- let notify_ntfy_topic: Option = row.get(4)?;
- let created_at: chrono::DateTime = row.get(5)?;
-
- // Parse the 'fields' JSON string
- let fields: serde_json::Value = serde_json::from_str(&fields_str).map_err(|e| {
- log::error!(
- "DB Parse Error: Failed to parse 'fields' JSON for form id {}: {}. Skipping this form.",
- id,
- e
- );
- rusqlite::Error::FromSqlConversionFailure(
- 2,
- rusqlite::types::Type::Text,
- Box::new(e),
- )
- })?;
-
- Ok(Form {
- id: Some(id),
- name,
- fields,
- notify_email,
- notify_ntfy_topic,
- created_at,
- })
- })
- .map_err(|e| {
- log::error!("Failed to execute query: {}", e);
- actix_web::error::ErrorInternalServerError("Database error")
- })?;
-
- // Collect results, filtering out rows that failed parsing
- let forms: Vec