From 2ac4fda94490d7e7b143c4a428cc4ccbfb2403d1 Mon Sep 17 00:00:00 2001 From: mohamad Date: Fri, 16 May 2025 02:10:41 +0200 Subject: [PATCH] Refactor project structure to transition from Rust to Node.js, removing Rust-specific files and adding Node.js dependencies. Introduced Docker support with a new Dockerfile and docker-compose.yml. Added server.js for Express application setup, along with necessary middleware and routes. Updated .gitignore to exclude environment files and SQLite database. Removed legacy files including Cargo.toml, Cargo.lock, and design.html. --- .gitignore | 5 +- Cargo.lock | 4102 ------------------------------- Cargo.toml | 39 - Dockerfile | 59 +- README.md | 149 -- config/default.toml | 30 - design.html | 1294 ---------- docker-compose.yml | 13 + form_data.db | Bin 36864 -> 0 bytes frontend/index.html | 220 -- frontend/script.js | 575 ----- frontend/style.css | 411 ---- package.json | 25 + server.js | 53 + src/auth.rs | 101 - src/config/database.js | 34 + src/db.rs | 356 --- src/handlers.rs | 751 ------ src/main.rs | 241 -- src/middleware/domainChecker.js | 49 + src/middleware/rateLimiter.js | 44 + src/models.rs | 76 - src/notifications.rs | 148 -- src/routes/admin.js | 405 +++ src/routes/public.js | 104 + src/services/notification.js | 33 + tests/handlers_test.rs | 1 - 27 files changed, 786 insertions(+), 8532 deletions(-) delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100644 README.md delete mode 100644 config/default.toml delete mode 100644 design.html create mode 100644 docker-compose.yml delete mode 100644 form_data.db delete mode 100644 frontend/index.html delete mode 100644 frontend/script.js delete mode 100644 frontend/style.css create mode 100644 package.json create mode 100644 server.js delete mode 100644 src/auth.rs create mode 100644 src/config/database.js delete mode 100644 src/db.rs delete mode 100644 src/handlers.rs delete mode 100644 src/main.rs create mode 100644 src/middleware/domainChecker.js create mode 100644 src/middleware/rateLimiter.js delete mode 100644 src/models.rs delete mode 100644 src/notifications.rs create mode 100644 src/routes/admin.js create mode 100644 src/routes/public.js create mode 100644 src/services/notification.js delete mode 100644 tests/handlers_test.rs 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 - - - - -
-
- - -
-
- - - - - 3 -
-
JD
-
-
-
- - -
-
- - -
-

Dashboard Overview

- -
- - -
-
-
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 -
-
-
- - -
- - -
-
-

- - - - - - - - Recent Forms -

- View All Forms -
- -
-
-
-
-
- -
- -
-
-
Customer Feedback Q2
-
- - - - - -
-
-
-
-
- 486 - Submissions -
-
- 75% - Completion -
-
-
-
-
- -
-
- - -
-
-
Annual Conf Registration
-
- - - - - -
-
-
-
-
- 312 - Submissions -
-
- 92% - Completion -
-
-
-
-
- -
-
- - -
-
-
Frontend Dev Application
-
- - - - - -
-
-
-
-
- 124 - Submissions -
-
- 88% - Completion -
-
-
-
-
- -
-
-
-
-
- - -
-
-

- - - - - - - - - Recent Submissions -

- - View All Submissions - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Form NameSubmitted byDateStatusActions
Customer Feedback Q2john.doe@example.comMay 05, 2025
New
- -
Annual Conf Registrationsarah.smith@example.comMay 04, 2025
Pending
- -
Customer Feedback Q2mark.rivera@sample.netMay 03, 2025
Reviewed
- -
-
-
-
- -
- - - - - 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 a2884b87fb860a05471274b6ea5c3b2ebf0b93a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36864 zcmeI*OHboQ00(e;c?9U9bPp)kkw7ihuBo4SNv$>z(+v#?o7jlms*1+(q*$95u_4j?G^*yCkf&{#?BKn=rSqBrN`w{5|D5P$##AOHafK;VBCc=_2(B9%&sukVM{ZL{d+Fo;L98#Sw7Tf{D` zm#z4wF(9i8WUTW&Vq1GQ*{PMb3$-WY8|%q~i2z;NVZ$Jms!b|RxjebO<+HXIoC`r@ znbgDHfCXf$Uab(1wfwFh`n@jcwc>P;VZ4b$H3y7_%xmx)+agc3UD~!t(b_CHWt(g` zwVGA28_~+TUD)0k#e6`zz0hwxZ?F#a+i^lhtGXdyANKlwGhQ#ftR)huKlckHA;kJ&dsJu%c9_SJ$4)% zwE6Q%gC2&xc)4+w_MoATEzBqX7XFDU?JMUNR$57#olSk2x(F>kc^hgxNxTxp`0W0P z7d=0_4+Ay`;?bRniW;xFZWWCpy?cfer&4h4wPd{By3N7! zen|Fdu+Kbl8h2D_qF7u=u1s*~6;il#0^3 zyKb=KzRxc~6W5coL-1YOeuo7i?exjfK2J(?h+(2mVvhDHNz?yTTrqt^k0&$I)a607 zIOw!==klOx9CV6*qRd5~DvNv&lUoAs(Ln$L5P$##AOHafKmY;|fB*y_aH+rxT}(Ks z7c)-XQ*||~n2Mavc&cpZt|#ZRd}NxL7B#cXb=@rWI(~OWb5~StTg>;c=|b26K>+F#pi`-Y}An|fN4b4`WGMlNm2 zZZ_MLT}@F~I!AR?%V>&b$_nq(wrb=JBd?^_48>3lJ*z196(z4I3p`(9@(+Rc=pX^a zlTF1?(#?#P)0^JD|Lu39zXxCvRm*Ei-bkbU|900P - - - - - Formies - - - - - - -
- -
- -

Formies - Simple Form Manager

- - -
-

Login

-
-
- - -
-
- - -
- - -
-
- - - - - -
-
-

Submit to a Form

-

Enter a Form ID to load and submit:

-
- - - -
- - -
-
- - - - - - - - - - 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 = ''; // 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
    = forms_iter - .filter_map(|result| match result { - Ok(form) => Some(form), - Err(e) => { - log::warn!("Skipping a form row due to a processing error: {}", e); - None - } - }) - .collect(); - - log::debug!("Returning {} forms for user {}", forms.len(), auth.user_id); - Ok(HttpResponse::Ok().json(forms)) -} - -// GET /forms/{form_id}/submissions -pub async fn get_submissions( - app_state: web::Data, - auth: Auth, // Requires authentication - path: web::Path, // Extracts form_id from the path -) -> ActixResult { - let form_id = path.into_inner(); - log::info!( - "User {} requesting submissions for form_id: {}", - auth.user_id, - form_id - ); - - let conn = app_state.db.lock().map_err(|e| { - log::error!("Failed to acquire database lock: {}", e); - actix_web::error::ErrorInternalServerError("Database error") - })?; - - // Check if the form exists - let _form = Form::get_by_id(&conn, &form_id).map_err(|e| { - if e.to_string().contains("not found") { - actix_web::error::ErrorNotFound("Form not found") - } else { - actix_web::error::ErrorInternalServerError("Database error") - } - })?; - - // Get submissions - let mut stmt = conn - .prepare( - "SELECT id, form_id, data, created_at FROM submissions WHERE form_id = ?1 ORDER BY created_at DESC", - ) - .map_err(|e| { - log::error!("Failed to prepare statement: {}", e); - actix_web::error::ErrorInternalServerError("Database error") - })?; - - let submissions_iter = stmt - .query_map(params![form_id], |row| { - let id: String = row.get(0)?; - let form_id: String = row.get(1)?; - let data_str: String = row.get(2)?; - let created_at: chrono::DateTime = row.get(3)?; - - let data: serde_json::Value = serde_json::from_str(&data_str).map_err(|e| { - log::error!( - "DB Parse Error: Failed to parse 'data' JSON for submission_id {}: {}. Skipping.", - id, - e - ); - rusqlite::Error::FromSqlConversionFailure( - 2, - rusqlite::types::Type::Text, - Box::new(e), - ) - })?; - - Ok(Submission { - id, - form_id, - data, - created_at, - }) - }) - .map_err(|e| { - log::error!("Failed to execute query: {}", e); - actix_web::error::ErrorInternalServerError("Database error") - })?; - - let submissions: Vec = submissions_iter - .filter_map(|result| match result { - Ok(submission) => Some(submission), - Err(e) => { - log::warn!("Skipping a submission row due to processing error: {}", e); - None - } - }) - .collect(); - - log::debug!( - "Returning {} submissions for form {} requested by user {}", - submissions.len(), - form_id, - auth.user_id - ); - Ok(HttpResponse::Ok().json(submissions)) -} - -// --- Notification Settings Handlers --- - -// GET /forms/{form_id}/notifications -pub async fn get_notification_settings( - app_state: web::Data, - auth: Auth, // Requires authentication - path: web::Path, -) -> ActixResult { - let form_id = path.into_inner(); - log::info!( - "User {} requesting notification settings for form_id: {}", - auth.user_id, - form_id - ); - - let conn = app_state.db.lock().map_err(|e| { - log::error!( - "Failed to acquire database lock for get_notification_settings: {}", - e - ); - actix_web::error::ErrorInternalServerError("Database error") - })?; - - // Get the form to ensure it exists and retrieve current settings - let form = Form::get_by_id(&conn, &form_id).map_err(|e| { - log::warn!( - "Attempt to get settings for non-existent form {}: {}", - form_id, - e - ); - if e.to_string().contains("not found") { - actix_web::error::ErrorNotFound("Form not found") - } else { - actix_web::error::ErrorInternalServerError("Database error retrieving form") - } - })?; - - let settings = crate::models::NotificationSettingsPayload { - notify_email: form.notify_email, - notify_ntfy_topic: form.notify_ntfy_topic, - }; - - Ok(HttpResponse::Ok().json(settings)) -} - -// PUT /forms/{form_id}/notifications -pub async fn update_notification_settings( - app_state: web::Data, - auth: Auth, // Requires authentication - path: web::Path, - payload: web::Json, -) -> ActixResult { - let form_id = path.into_inner(); - let new_settings = payload.into_inner(); - log::info!( - "User {} updating notification settings for form_id: {}. Settings: {:?}", - auth.user_id, - form_id, - new_settings - ); - - let conn = app_state.db.lock().map_err(|e| { - log::error!( - "Failed to acquire database lock for update_notification_settings: {}", - e - ); - actix_web::error::ErrorInternalServerError("Database error") - })?; - - // Fetch the existing form to update it - let mut form = Form::get_by_id(&conn, &form_id).map_err(|e| { - log::warn!( - "Attempt to update settings for non-existent form {}: {}", - form_id, - e - ); - if e.to_string().contains("not found") { - actix_web::error::ErrorNotFound("Form not found") - } else { - actix_web::error::ErrorInternalServerError("Database error retrieving form") - } - })?; - - // Update the form fields - form.notify_email = new_settings.notify_email; - form.notify_ntfy_topic = new_settings.notify_ntfy_topic; - - // Save the updated form - form.save(&conn).map_err(|e| { - log::error!( - "Failed to save updated notification settings for form {}: {}", - form_id, - e - ); - actix_web::error::ErrorInternalServerError("Failed to save notification settings") - })?; - - log::info!( - "Successfully updated notification settings for form {}", - form_id - ); - Ok(HttpResponse::Ok().json(json!({ "message": "Notification settings updated successfully" }))) -} - -pub async fn health_check() -> impl Responder { - HttpResponse::Ok().json(serde_json::json!({ - "status": "ok", - "version": env!("CARGO_PKG_VERSION"), - "timestamp": chrono::Utc::now().to_rfc3339() - })) -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index af0a0e9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,241 +0,0 @@ -// src/main.rs -use actix_cors::Cors; -use actix_files as fs; -use actix_route_rate_limiter::{Limiter, RateLimiter}; -use actix_web::{http::header, middleware::Logger, web, App, HttpServer}; -use config::{Config, Environment}; -use dotenv::dotenv; -use std::env; -use std::io::Result as IoResult; -use std::process; -use std::sync::{Arc, Mutex}; -use std::time::Duration; -use tracing::{error, info, warn}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -// Import modules -mod auth; -mod db; -mod handlers; -mod models; -mod notifications; - -use notifications::{NotificationConfig, NotificationService}; - -// Application state that will be shared across all routes -pub struct AppState { - db: Arc>, - notification_service: Arc, -} - -#[actix_web::main] -async fn main() -> IoResult<()> { - // Load environment variables from .env file - dotenv().ok(); - - // Initialize Sentry for error tracking - let _guard = sentry::init(( - env::var("SENTRY_DSN").unwrap_or_default(), - sentry::ClientOptions { - release: sentry::release_name!(), - ..Default::default() - }, - )); - - // Initialize structured logging - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::new( - env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), - )) - .with(tracing_subscriber::fmt::layer()) - .init(); - - // Load configuration - let settings = Config::builder() - .add_source(Environment::default()) - .build() - .unwrap_or_else(|e| { - error!("Failed to load configuration: {}", e); - process::exit(1); - }); - - // --- Configuration (Environment Variables) --- - let database_url = settings.get_string("DATABASE_URL").unwrap_or_else(|_| { - warn!("DATABASE_URL environment variable not set. Defaulting to 'form_data.db'."); - "form_data.db".to_string() - }); - - let bind_address = settings.get_string("BIND_ADDRESS").unwrap_or_else(|_| { - warn!("BIND_ADDRESS environment variable not set. Defaulting to '127.0.0.1:8080'."); - "127.0.0.1:8080".to_string() - }); - - // Read allowed origins as a comma-separated string, defaulting to empty - let allowed_origins_str = env::var("ALLOWED_ORIGIN").unwrap_or_else(|_| { - warn!("ALLOWED_ORIGIN environment variable not set. CORS will be restrictive."); - String::new() // Default to empty string if not set - }); - - // Split the string into a vector of origins - let allowed_origins_list: Vec = if allowed_origins_str.is_empty() { - Vec::new() // Return an empty vector if the string is empty - } else { - allowed_origins_str - .split(',') - .map(|s| s.trim().to_string()) // Trim whitespace and convert to String - .filter(|s| !s.is_empty()) // Remove empty strings resulting from extra commas - .collect() - }; - - info!(" --- Formies Backend Configuration ---"); - info!("Required Environment Variables:"); - info!(" - DATABASE_URL (Current: {})", database_url); - info!(" - BIND_ADDRESS (Current: {})", bind_address); - info!(" - INITIAL_ADMIN_USERNAME (Set during startup, MUST be present)"); - info!(" - INITIAL_ADMIN_PASSWORD (Set during startup, MUST be present)"); - info!("Optional Environment Variables:"); - if !allowed_origins_list.is_empty() { - info!( - " - ALLOWED_ORIGIN (Set: {})", - allowed_origins_list.join(", ") // Log the list nicely - ); - } else { - warn!(" - ALLOWED_ORIGIN (Not Set): CORS will be restrictive"); - } - info!(" - RUST_LOG (e.g., 'info,formies_be=debug')"); - info!(" --- End Configuration ---"); - - // Initialize database connection - let db_connection = match db::init_db(&database_url) { - Ok(conn) => conn, - Err(e) => { - if e.to_string().contains("INITIAL_ADMIN_USERNAME") - || e.to_string().contains("INITIAL_ADMIN_PASSWORD") - { - error!("FATAL: {}", e); - error!("Please set the INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD environment variables."); - } else { - error!( - "FATAL: Failed to initialize database at {}: {:?}", - database_url, e - ); - } - process::exit(1); - } - }; - - // Initialize rate limiter using the correct fields - let limiter = Limiter { - ip_addresses: std::collections::HashMap::new(), // Stores IP request counts - duration: chrono::TimeDelta::from_std(Duration::from_secs(60)).expect("Invalid duration"), // Convert std::time::Duration - num_requests: 100, // Max requests allowed in the duration - }; - // Create the cloneable Arc> outside the closure - let limiter_data = Arc::new(Mutex::new(limiter)); - - // Initialize notification service - let notification_config = NotificationConfig::from_env().unwrap_or_else(|e| { - warn!( - "Failed to load notification configuration: {}. Notifications will not be available.", - e - ); - NotificationConfig::default() - }); - let notification_service = Arc::new(NotificationService::new(notification_config)); - - // Create AppState with both database and notification service - let app_state = web::Data::new(AppState { - db: Arc::new(Mutex::new(db_connection)), - notification_service: notification_service.clone(), - }); - - info!("Starting server at http://{}", bind_address); - - HttpServer::new(move || { - let app_state = app_state.clone(); - let allowed_origins = allowed_origins_list.clone(); - let rate_limiter = RateLimiter::new(limiter_data.clone()); - - // Configure CORS - let cors = if !allowed_origins.is_empty() { - info!("Configuring CORS for origins: {:?}", allowed_origins); - let mut cors = Cors::default(); - for origin in allowed_origins { - cors = cors.allowed_origin(&origin); // Add each origin - } - cors.allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) - .allowed_headers(vec![ - header::AUTHORIZATION, - header::ACCEPT, - header::CONTENT_TYPE, - header::ORIGIN, - header::ACCESS_CONTROL_REQUEST_METHOD, - header::ACCESS_CONTROL_REQUEST_HEADERS, - ]) - .supports_credentials() - .max_age(3600) - } else { - warn!("CORS is configured restrictively: No ALLOWED_ORIGIN set."); - Cors::default() // Keep restrictive default if no origins are provided - .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) - .allowed_headers(vec![ - header::AUTHORIZATION, - header::ACCEPT, - header::CONTENT_TYPE, - header::ORIGIN, - header::ACCESS_CONTROL_REQUEST_METHOD, - header::ACCESS_CONTROL_REQUEST_HEADERS, - ]) - .supports_credentials() - .max_age(3600) - }; - - App::new() - .wrap(cors) - .wrap(Logger::default()) - .wrap(tracing_actix_web::TracingLogger::default()) - .wrap(rate_limiter) - .app_data(app_state) - .service( - web::scope("/api") - // Health check endpoint - .route("/health", web::get().to(handlers::health_check)) - // Public routes - .route("/login", web::post().to(handlers::login)) - .route( - "/forms/{form_id}/submissions", - web::post().to(handlers::submit_form), - ) - // Protected routes - .route("/logout", web::post().to(handlers::logout)) - .route("/forms", web::post().to(handlers::create_form)) - .route("/forms", web::get().to(handlers::get_forms)) - .route( - "/forms/{form_id}/submissions", - web::get().to(handlers::get_submissions), - ) - .route( - "/forms/{form_id}/notifications", - web::get().to(handlers::get_notification_settings), - ) - .route( - "/forms/{form_id}/notifications", - web::put().to(handlers::update_notification_settings), - ), - ) - .service( - fs::Files::new("/", "./frontend/") - .index_file("index.html") - .use_last_modified(true) - .default_handler(fs::NamedFile::open("./frontend/index.html").unwrap_or_else( - |_| { - error!("Fallback file not found: ../frontend/index.html"); - process::exit(1); - }, - )), - ) - }) - .bind(&bind_address)? - .run() - .await -} diff --git a/src/middleware/domainChecker.js b/src/middleware/domainChecker.js new file mode 100644 index 0000000..f0a9039 --- /dev/null +++ b/src/middleware/domainChecker.js @@ -0,0 +1,49 @@ +import dbPromise from "../config/database.js"; + +const domainChecker = async (req, res, next) => { + const formUuid = req.params.formUuid; + const referer = req.headers.referer || req.headers.origin; + + try { + const db = await dbPromise; + const form = await db.get( + "SELECT allowed_domains FROM forms WHERE uuid = ?", + [formUuid] + ); + + if (!form) { + return res.status(404).json({ error: "Form not found" }); + } + + // If no domains are specified, allow all + if (!form.allowed_domains) { + return next(); + } + + const allowedDomains = form.allowed_domains.split(",").map((d) => d.trim()); + + if (!referer) { + return res.status(403).json({ error: "Referer header is required" }); + } + + const refererUrl = new URL(referer); + const isAllowed = allowedDomains.some( + (domain) => + refererUrl.hostname === domain || + refererUrl.hostname.endsWith("." + domain) + ); + + if (!isAllowed) { + return res + .status(403) + .json({ error: "Submission not allowed from this domain" }); + } + + next(); + } catch (error) { + console.error("Domain check error:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +export default domainChecker; diff --git a/src/middleware/rateLimiter.js b/src/middleware/rateLimiter.js new file mode 100644 index 0000000..1e39dae --- /dev/null +++ b/src/middleware/rateLimiter.js @@ -0,0 +1,44 @@ +const ipRateLimitStore = new Map(); + +// Clean up old entries every 5 minutes +setInterval(() => { + const now = Date.now(); + for (const [key, value] of ipRateLimitStore.entries()) { + if (now - value.timestamp > 60000) { + // 1 minute + ipRateLimitStore.delete(key); + } + } +}, 5 * 60 * 1000); + +const rateLimiter = (req, res, next) => { + const formUuid = req.params.formUuid; + const ip = req.ip; + const key = `${formUuid}_${ip}`; + const now = Date.now(); + const windowMs = 60000; // 1 minute + const maxRequests = 5; // 5 requests per minute + + const current = ipRateLimitStore.get(key) || { count: 0, timestamp: now }; + + // Reset if window has passed + if (now - current.timestamp > windowMs) { + current.count = 0; + current.timestamp = now; + } + + // Check if limit exceeded + if (current.count >= maxRequests) { + return res.status(429).json({ + error: "Too many requests. Please try again later.", + }); + } + + // Increment counter + current.count++; + ipRateLimitStore.set(key, current); + + next(); +}; + +export default rateLimiter; diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index 3562944..0000000 --- a/src/models.rs +++ /dev/null @@ -1,76 +0,0 @@ -// src/models.rs -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -// Consider adding chrono for DateTime types if needed in responses -// use chrono::{DateTime, Utc}; - -// Represents the structure for defining a form -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Form { - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - pub name: String, - /// Stores the structure defining the form fields. - /// Expected to be a JSON array of field definition objects. - /// Example field definition object: - /// ```json - /// { - /// "name": "email", // String, required: Unique identifier for the field - /// "type": "email", // String, required: "string", "number", "boolean", "email", "url", "object", "array" - /// "label": "Email Address", // String, optional: User-friendly label - /// "required": true, // Boolean, optional (default: false): If the field must have a value - /// "placeholder": "you@example.com", // String, optional: Placeholder text - /// "minLength": 5, // Number, optional: Minimum length for strings - /// "maxLength": 100, // Number, optional: Maximum length for strings - /// "min": 0, // Number, optional: Minimum value for numbers - /// "max": 100, // Number, optional: Maximum value for numbers - /// "pattern": "^\\S+@\\S+\\.\\S+$" // String, optional: Regex pattern for strings (e.g., email, url types might use this implicitly or explicitly) - /// // Add other properties like "options" for select/radio, etc. - /// } - /// ``` - pub fields: serde_json::Value, - pub notify_email: Option, - pub notify_ntfy_topic: Option, - pub created_at: DateTime, -} - -// Represents a single submission for a specific form -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Submission { - pub id: String, - pub form_id: String, - /// Stores the data submitted by the user. - /// Expected to be a JSON object where keys are field names (`name`) from the form definition's `fields` array. - /// Example: `{ "email": "user@example.com", "age": 30 }` - pub data: serde_json::Value, - pub created_at: DateTime, -} - -// Used for the /login endpoint request body -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginCredentials { - pub username: String, - pub password: String, -} - -// Used for the /login endpoint response body -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginResponse { - pub token: String, // The session token (UUID) -} - -// Used internally to represent a user fetched from the DB for authentication check -// Not serialized, only used within db.rs and handlers.rs -#[derive(Debug)] -pub struct UserAuthData { - pub id: String, - pub hashed_password: String, - // Note: Token and expiry are handled separately and not needed in this specific struct -} - -// Used for the GET/PUT /forms/{form_id}/notifications endpoints -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct NotificationSettingsPayload { - pub notify_email: Option, - pub notify_ntfy_topic: Option, -} diff --git a/src/notifications.rs b/src/notifications.rs deleted file mode 100644 index 8f0503e..0000000 --- a/src/notifications.rs +++ /dev/null @@ -1,148 +0,0 @@ -use anyhow::Result; -use lettre::message::header::ContentType; -use lettre::transport::smtp::authentication::Credentials; -use lettre::{Message, SmtpTransport, Transport}; -use serde::Serialize; -use std::env; - -#[derive(Debug, Serialize)] -pub struct NotificationConfig { - smtp_host: String, - smtp_port: u16, - smtp_username: String, - smtp_password: String, - from_email: String, - ntfy_topic: String, - ntfy_server: String, -} - -impl Default for NotificationConfig { - fn default() -> Self { - Self { - smtp_host: String::new(), - smtp_port: 587, - smtp_username: String::new(), - smtp_password: String::new(), - from_email: String::new(), - ntfy_topic: String::new(), - ntfy_server: "https://ntfy.sh".to_string(), - } - } -} - -impl NotificationConfig { - pub fn from_env() -> Result { - Ok(Self { - smtp_host: env::var("SMTP_HOST")?, - smtp_port: env::var("SMTP_PORT")?.parse()?, - smtp_username: env::var("SMTP_USERNAME")?, - smtp_password: env::var("SMTP_PASSWORD")?, - from_email: env::var("FROM_EMAIL")?, - ntfy_topic: env::var("NTFY_TOPIC")?, - ntfy_server: env::var("NTFY_SERVER").unwrap_or_else(|_| "https://ntfy.sh".to_string()), - }) - } - - pub fn is_email_configured(&self) -> bool { - !self.smtp_host.is_empty() - && !self.smtp_username.is_empty() - && !self.smtp_password.is_empty() - && !self.from_email.is_empty() - } - - pub fn is_ntfy_configured(&self) -> bool { - !self.ntfy_topic.is_empty() - } -} - -pub struct NotificationService { - config: NotificationConfig, -} - -impl NotificationService { - pub fn new(config: NotificationConfig) -> Self { - Self { config } - } - - pub async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<()> { - if !self.config.is_email_configured() { - return Ok(()); - } - - let email = Message::builder() - .from(self.config.from_email.parse()?) - .to(to.parse()?) - .subject(subject) - .header(ContentType::TEXT_PLAIN) - .body(body.to_string())?; - - let creds = Credentials::new( - self.config.smtp_username.clone(), - self.config.smtp_password.clone(), - ); - - let mailer = SmtpTransport::relay(&self.config.smtp_host)? - .port(self.config.smtp_port) - .credentials(creds) - .build(); - - mailer.send(&email)?; - Ok(()) - } - - pub fn send_ntfy(&self, title: &str, message: &str, priority: Option) -> Result<()> { - if !self.config.is_ntfy_configured() { - return Ok(()); - } - - let url = format!("{}/{}", self.config.ntfy_server, self.config.ntfy_topic); - - let mut request = ureq::post(&url).set("Title", title); - - if let Some(p) = priority { - request = request.set("Priority", &p.to_string()); - } - - request.send_string(message)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_notification_config() { - std::env::set_var("SMTP_HOST", "smtp.example.com"); - std::env::set_var("SMTP_PORT", "587"); - std::env::set_var("SMTP_USERNAME", "test@example.com"); - std::env::set_var("SMTP_PASSWORD", "password"); - std::env::set_var("FROM_EMAIL", "noreply@example.com"); - std::env::set_var("NTFY_TOPIC", "my-topic"); - - let config = NotificationConfig::from_env().unwrap(); - assert_eq!(config.smtp_host, "smtp.example.com"); - assert_eq!(config.smtp_port, 587); - assert_eq!(config.ntfy_server, "https://ntfy.sh"); - } - - #[test] - fn test_config_validation() { - let default_config = NotificationConfig::default(); - assert!(!default_config.is_email_configured()); - assert!(!default_config.is_ntfy_configured()); - - let config = NotificationConfig { - smtp_host: "smtp.example.com".to_string(), - smtp_port: 587, - smtp_username: "user".to_string(), - smtp_password: "pass".to_string(), - from_email: "test@example.com".to_string(), - ntfy_topic: "topic".to_string(), - ntfy_server: "https://ntfy.sh".to_string(), - }; - assert!(config.is_email_configured()); - assert!(config.is_ntfy_configured()); - } -} diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000..9e2df7b --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,405 @@ +import express from "express"; +import { v4 as uuidv4 } from "uuid"; +import dbPromise from "../config/database.js"; +import { sendNtfyNotification } from "../services/notification.js"; + +const router = express.Router(); + +router.get("/", async (req, res) => { + try { + const db = await dbPromise; + const forms = await db.all(` + SELECT f.uuid, f.name, f.created_at, f.is_archived, + (SELECT COUNT(*) FROM submissions WHERE form_uuid = f.uuid) as submission_count + FROM forms f ORDER BY created_at DESC + `); + res.render("index", { + forms, + appUrl: `${req.protocol}://${req.get("host")}`, + }); + } catch (error) { + console.error("Error fetching forms:", error); + res.status(500).send("Error fetching forms"); + } +}); + +router.post("/create-form", async (req, res) => { + const formName = req.body.formName || "Untitled Form"; + const newUuid = uuidv4(); + try { + const db = await dbPromise; + await db.run("INSERT INTO forms (uuid, name) VALUES (?, ?)", [newUuid, formName]); + console.log(`Form created: ${formName} with UUID: ${newUuid}`); + await sendNtfyNotification( + "New Form Created", + `Form "${formName}" (UUID: ${newUuid}) was created.`, + "high" + ); + res.redirect("/admin"); + } catch (error) { + console.error("Error creating form:", error); + res.status(500).send("Error creating form"); + } +}); + +router.get("/submissions/:formUuid", async (req, res) => { + const { formUuid } = req.params; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const offset = (page - 1) * limit; + + try { + const db = await dbPromise; + const formDetails = await db.get("SELECT name FROM forms WHERE uuid = ?", [formUuid]); + if (!formDetails) { + return res.status(404).send("Form not found."); + } + + // Get total count for pagination + const countResult = await db.get( + "SELECT COUNT(*) as total FROM submissions WHERE form_uuid = ?", + [formUuid] + ); + const totalSubmissions = countResult.total; + const totalPages = Math.ceil(totalSubmissions / limit); + + const submissions = await db.all( + "SELECT id, data, ip_address, submitted_at FROM submissions WHERE form_uuid = ? ORDER BY submitted_at DESC LIMIT ? OFFSET ?", + [formUuid, limit, offset] + ); + + res.render("submissions", { + submissions, + formUuid, + formName: formDetails.name, + appUrl: `${req.protocol}://${req.get("host")}`, + pagination: { + currentPage: page, + totalPages, + totalSubmissions, + limit, + }, + }); + } catch (error) { + console.error("Error fetching submissions:", error); + res.status(500).send("Error fetching submissions"); + } +}); + +router.get("/submissions/:formUuid/export", async (req, res) => { + const { formUuid } = req.params; + try { + const db = await dbPromise; + const formDetails = await db.get("SELECT name FROM forms WHERE uuid = ?", [formUuid]); + if (!formDetails) { + return res.status(404).send("Form not found."); + } + + const submissions = await db.all( + "SELECT data, ip_address, submitted_at FROM submissions WHERE form_uuid = ? ORDER BY submitted_at DESC", + [formUuid] + ); + + // Create CSV content + const headers = ["Submitted At", "IP Address"]; + const rows = submissions.map((submission) => { + const data = JSON.parse(submission.data); + // Add all form fields as headers + Object.keys(data).forEach((key) => { + if (!headers.includes(key)) { + headers.push(key); + } + }); + return { + submitted_at: new Date(submission.submitted_at).toISOString(), + ip_address: submission.ip_address, + ...data, + }; + }); + + // Generate CSV content + let csvContent = headers.join(",") + "\n"; + rows.forEach((row) => { + const values = headers.map((header) => { + const value = row[header] || ""; + // Escape commas and quotes in values + return `"${String(value).replace(/"/g, '""')}"`; + }); + csvContent += values.join(",") + "\n"; + }); + + // Set response headers for CSV download + res.setHeader("Content-Type", "text/csv"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${formDetails.name}-submissions.csv"` + ); + res.send(csvContent); + } catch (error) { + console.error("Error exporting submissions:", error); + res.status(500).send("Error exporting submissions"); + } +}); + +router.post("/delete-form/:formUuid", async (req, res) => { + const { formUuid } = req.params; + try { + const db = await dbPromise; + const formResult = await db.all("SELECT name FROM forms WHERE uuid = ?", [formUuid]); + const formName = + formResult.length > 0 ? formResult[0].name : `Form ${formUuid}`; + + await db.run("DELETE FROM forms WHERE uuid = ?", [formUuid]); + console.log(`Form ${formUuid} deleted.`); + await sendNtfyNotification( + "Form Deleted", + `Form "${formName}" (UUID: ${formUuid}) was deleted.`, + "default" + ); + res.redirect("/admin"); + } catch (error) { + console.error("Error deleting form:", error); + res.status(500).send("Error deleting form."); + } +}); + +router.post("/delete-submission/:submissionId", async (req, res) => { + const { submissionId } = req.params; + let formUuidForRedirect = "unknown-form"; + try { + const db = await dbPromise; + const submissionResult = await db.all("SELECT form_uuid FROM submissions WHERE id = ?", [submissionId]); + if (submissionResult.length > 0) { + formUuidForRedirect = submissionResult[0].form_uuid; + } + + await db.run("DELETE FROM submissions WHERE id = ?", [submissionId]); + console.log(`Submission ${submissionId} deleted.`); + await sendNtfyNotification( + "Submission Deleted", + `Submission ID ${submissionId} (for form ${formUuidForRedirect}) was deleted.`, + "low" + ); + if (formUuidForRedirect !== "unknown-form") { + res.redirect(`/admin/submissions/${formUuidForRedirect}`); + } else { + res.redirect("/admin"); + } + } catch (error) { + console.error("Error deleting submission:", error); + res.status(500).send("Error deleting submission."); + if (formUuidForRedirect !== "unknown-form") { + res.redirect(`/admin/submissions/${formUuidForRedirect}`); + } else { + res.redirect("/admin"); + } + } +}); + +router.post("/update-form-name/:formUuid", async (req, res) => { + const { formUuid } = req.params; + const { newName } = req.body; + + if (!newName || newName.trim() === "") { + return res.status(400).send("New form name is required."); + } + + try { + const db = await dbPromise; + const formResult = await db.all("SELECT name FROM forms WHERE uuid = ?", [formUuid]); + + if (formResult.length === 0) { + return res.status(404).send("Form not found."); + } + + const oldName = formResult[0].name; + + await db.run("UPDATE forms SET name = ? WHERE uuid = ?", [ + newName, + formUuid, + ]); + + console.log( + `Form name updated from '${oldName}' to '${newName}' for UUID: ${formUuid}` + ); + + await sendNtfyNotification( + "Form Name Updated", + `Form name changed from '${oldName}' to '${newName}' for UUID: ${formUuid}.`, + "default" + ); + + res.redirect("/admin"); + } catch (error) { + console.error("Error updating form name:", error); + res.status(500).send("Error updating form name."); + } +}); + +router.post("/update-thank-you-url/:formUuid", async (req, res) => { + const { formUuid } = req.params; + const { thankYouUrl } = req.body; + + try { + const db = await dbPromise; + await db.run("UPDATE forms SET thank_you_url = ? WHERE uuid = ?", [thankYouUrl, formUuid]); + console.log(`Thank You URL updated for form UUID: ${formUuid}`); + res.redirect("/admin"); + } catch (error) { + console.error("Error updating Thank You URL:", error); + res.status(500).send("Error updating Thank You URL."); + } +}); + +router.post("/update-ntfy-enabled/:formUuid", async (req, res) => { + const { formUuid } = req.params; + const ntfyEnabled = req.body.ntfyEnabled === "true"; + + try { + const db = await dbPromise; + await db.run("UPDATE forms SET ntfy_enabled = ? WHERE uuid = ?", [ + ntfyEnabled, + formUuid, + ]); + console.log(`Ntfy notifications updated for form UUID: ${formUuid}`); + res.redirect("/admin"); + } catch (error) { + console.error("Error updating Ntfy notifications setting:", error); + res.status(500).send("Error updating Ntfy notifications setting."); + } +}); + +router.post("/clear-submissions/:formUuid", async (req, res) => { + const { formUuid } = req.params; + try { + // First get form name for notification before clearing + const db = await dbPromise; + const formResult = await db.all("SELECT name FROM forms WHERE uuid = ?", [formUuid]); + const formName = + formResult.length > 0 ? formResult[0].name : `Form ${formUuid}`; + + // Delete all submissions for this form + await db.run("DELETE FROM submissions WHERE form_uuid = ?", [formUuid]); + console.log(`All submissions cleared for form ${formUuid}`); + await sendNtfyNotification( + "Submissions Cleared", + `All submissions for form "${formName}" (UUID: ${formUuid}) were cleared.`, + "default" + ); + res.redirect(`/admin/submissions/${formUuid}`); + } catch (error) { + console.error("Error clearing submissions:", error); + res.status(500).send("Error clearing submissions."); + } +}); + +router.post("/update-thank-you-message/:formUuid", async (req, res) => { + const { formUuid } = req.params; + const { thankYouMessage } = req.body; + try { + const db = await dbPromise; + await db.run("UPDATE forms SET thank_you_message = ? WHERE uuid = ?", [thankYouMessage, formUuid]); + console.log(`Thank you message updated for form ${formUuid}`); + res.redirect("/admin"); + } catch (error) { + console.error("Error updating thank you message:", error); + res.status(500).send("Error updating thank you message."); + } +}); + +router.post("/archive-form/:formUuid", async (req, res) => { + const { formUuid } = req.params; + const { archive } = req.body; + + try { + const db = await dbPromise; + const formResult = await db.all("SELECT name FROM forms WHERE uuid = ?", [formUuid]); + + if (formResult.length === 0) { + return res.status(404).send("Form not found."); + } + + await db.run("UPDATE forms SET is_archived = ? WHERE uuid = ?", [ + archive === "true", + formUuid, + ]); + + const action = archive === "true" ? "archived" : "unarchived"; + await sendNtfyNotification( + `Form ${action}`, + `Form "${formResult[0].name}" (UUID: ${formUuid}) has been ${action}.`, + "default" + ); + + res.redirect("/admin"); + } catch (error) { + console.error("Error updating form archive status:", error); + res.status(500).send("Error updating form archive status"); + } +}); + +router.post("/update-allowed-domains/:formUuid", async (req, res) => { + const { formUuid } = req.params; + const { allowedDomains } = req.body; + + try { + const db = await dbPromise; + const formResult = await db.all("SELECT name FROM forms WHERE uuid = ?", [formUuid]); + + if (formResult.length === 0) { + return res.status(404).send("Form not found."); + } + + await db.run("UPDATE forms SET allowed_domains = ? WHERE uuid = ?", [ + allowedDomains, + formUuid, + ]); + + await sendNtfyNotification( + "Form Allowed Domains Updated", + `Form "${formResult[0].name}" (UUID: ${formUuid}) allowed domains have been updated.`, + "default" + ); + + res.redirect("/admin"); + } catch (error) { + console.error("Error updating allowed domains:", error); + res.status(500).send("Error updating allowed domains"); + } +}); + +router.post("/test-notification/:formUuid", async (req, res) => { + const { formUuid } = req.params; + + try { + const db = await dbPromise; + const formResult = await db.all("SELECT name, ntfy_enabled FROM forms WHERE uuid = ?", [formUuid]); + + if (formResult.length === 0) { + return res.status(404).send("Form not found."); + } + + if (!formResult[0].ntfy_enabled) { + return res + .status(400) + .send("Ntfy notifications are not enabled for this form."); + } + + await sendNtfyNotification( + "Test Notification", + `This is a test notification for form "${formResult[0].name}" (UUID: ${formUuid}).`, + "default", + "test" + ); + + res.json({ + success: true, + message: "Test notification sent successfully.", + }); + } catch (error) { + console.error("Error sending test notification:", error); + res.status(500).send("Error sending test notification"); + } +}); + +export default router; diff --git a/src/routes/public.js b/src/routes/public.js new file mode 100644 index 0000000..e150661 --- /dev/null +++ b/src/routes/public.js @@ -0,0 +1,104 @@ +import express from "express"; +import dbPromise from "../config/database.js"; +import { sendNtfyNotification } from "../services/notification.js"; +import rateLimiter from "../middleware/rateLimiter.js"; +import domainChecker from "../middleware/domainChecker.js"; + +const router = express.Router(); + +router.get("/", (req, res) => res.redirect("/admin")); +router.get("/health", (req, res) => res.status(200).json({ status: "ok" })); + +router.post( + "/submit/:formUuid", + rateLimiter, + domainChecker, + async (req, res) => { + const { formUuid } = req.params; + const submissionData = { ...req.body }; + const ipAddress = req.ip; + + if (submissionData.honeypot_field && submissionData.honeypot_field !== "") { + console.log( + `Honeypot triggered for ${formUuid} by IP ${ipAddress}. Ignoring submission.` + ); + if (submissionData._thankyou) { + return res.redirect(submissionData._thankyou); + } + return res.send( + "

    Thank You!

    Your submission has been received.

    " + ); + } + delete submissionData.honeypot_field; + + let formNameForNotification = `Form ${formUuid}`; + try { + const db = await dbPromise; + const form = await db.get( + "SELECT id, name, thank_you_url, thank_you_message, ntfy_enabled, is_archived FROM forms WHERE uuid = ?", + [formUuid] + ); + if (!form) { + return res.status(404).send("Form endpoint not found."); + } + + if (form.is_archived) { + return res + .status(410) + .send( + "This form has been archived and is no longer accepting submissions." + ); + } + + formNameForNotification = form.name || `Form ${formUuid}`; + const ntfyEnabled = form.ntfy_enabled; + + await db.run( + "INSERT INTO submissions (form_uuid, data, ip_address) VALUES (?, ?, ?)", + [formUuid, JSON.stringify(submissionData), ipAddress] + ); + console.log(`Submission received for ${formUuid}:`, submissionData); + + const submissionSummary = Object.entries(submissionData) + .filter(([key]) => key !== "_thankyou") + .map(([key, value]) => `${key}: ${value}`) + .join(", "); + + if (ntfyEnabled) { + await sendNtfyNotification( + `New Submission: ${formNameForNotification}`, + `Data: ${submissionSummary || "No data fields" + }\nFrom IP: ${ipAddress}`, + "high", + "incoming_form" + ); + } + + if (form.thank_you_url) { + return res.redirect(form.thank_you_url); + } + + if (form.thank_you_message) { + return res.send(`

    Thank You!

    ${form.thank_you_message}

    `); + } + + if (submissionData._thankyou) { + return res.redirect(submissionData._thankyou); + } + + res.send( + '

    Thank You!

    Your submission has been received.

    Back to formies

    ' + ); + } catch (error) { + console.error("Error processing submission:", error); + await sendNtfyNotification( + `Submission Error: ${formNameForNotification}`, + `Failed to process submission for ${formUuid} from IP ${ipAddress}. Error: ${error.message}`, + "max" + ); + res.status(500).send("Error processing submission."); + } + } +); + +export default router; diff --git a/src/services/notification.js b/src/services/notification.js new file mode 100644 index 0000000..312a98e --- /dev/null +++ b/src/services/notification.js @@ -0,0 +1,33 @@ +import fetch from "node-fetch"; + +export async function sendNtfyNotification( + title, + message, + priority = "default", + tags = "" +) { + if (process.env.NTFY_ENABLED !== "true" || !process.env.NTFY_TOPIC_URL) { + return; + } + try { + const response = await fetch(process.env.NTFY_TOPIC_URL, { + method: "POST", + body: message, + headers: { + Title: title, + Priority: priority, + Tags: tags, + "Content-Type": "text/plain", + }, + }); + if (!response.ok) { + console.error(`Ntfy error: ${response.status} ${await response.text()}`); + } else { + console.log("Ntfy notification sent successfully."); + } + } catch (error) { + console.error("Failed to send Ntfy notification:", error); + } +} + +export default { sendNtfyNotification }; diff --git a/tests/handlers_test.rs b/tests/handlers_test.rs deleted file mode 100644 index 8b13789..0000000 --- a/tests/handlers_test.rs +++ /dev/null @@ -1 +0,0 @@ -