From 545ae921e51678f3fc608bd8102e68eb1f0a43c6 Mon Sep 17 00:00:00 2001 From: Dom Date: Mon, 13 Apr 2026 15:55:36 +0200 Subject: [PATCH] feat: portage complet en Rust (axum + sysinfo + tera) Remplacement du backend Python/Flask par un binaire Rust natif. Stack : axum (web), sysinfo (metriques), lettre (SMTP), tera (templates), argon2 (auth). Binaire Windows 6.3 Mo sans dependance runtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + Cargo.lock | 2358 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 33 + src/alerter.rs | 86 ++ src/config.rs | 238 ++++ src/main.rs | 922 +++++++++++++++ src/monitor.rs | 344 ++++++ templates/alerts.html | 10 +- templates/base.html | 40 +- templates/dashboard.html | 23 +- templates/login.html | 12 +- templates/settings.html | 22 +- 12 files changed, 4029 insertions(+), 60 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/alerter.rs create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/monitor.rs diff --git a/.gitignore b/.gitignore index 000e99a..f42ed60 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ imput/ *.spec build/ dist/ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c98f631 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2358 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[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 = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[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 = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lettre" +version = "0.11.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" +dependencies = [ + "base64", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "socket2", + "tokio", + "url", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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 = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" +dependencies = [ + "bitflags", + "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", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[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.17", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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 = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supervision" +version = "1.0.0" +dependencies = [ + "argon2", + "axum", + "chrono", + "form_urlencoded", + "http", + "lettre", + "password-hash", + "rand", + "serde", + "serde_json", + "sysinfo", + "tera", + "tokio", + "tower 0.4.13", + "tower-http", + "uuid", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[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 = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..da70370 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "supervision" +version = "1.0.0" +edition = "2021" +description = "Monitoring systeme avec interface web securisee" + +[[bin]] +name = "supervision" +path = "src/main.rs" + +[dependencies] +axum = "0.7" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tera = "1" +sysinfo = "0.32" +lettre = "0.11" +argon2 = "0.5" +password-hash = "0.5" +tower = "0.4" +tower-http = { version = "0.5", features = ["fs"] } +rand = "0.8" +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v4"] } +form_urlencoded = "1" +http = "1" + +[profile.release] +opt-level = 3 +lto = true +strip = true +codegen-units = 1 diff --git a/src/alerter.rs b/src/alerter.rs new file mode 100644 index 0000000..134f6ca --- /dev/null +++ b/src/alerter.rs @@ -0,0 +1,86 @@ +use crate::config::SmtpConfig; +use lettre::message::header::ContentType; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport, Transport}; + +pub fn is_configured(smtp: &SmtpConfig) -> bool { + !smtp.server.is_empty() && !smtp.from_email.is_empty() && !smtp.to_emails.is_empty() +} + +pub fn send_email(smtp: &SmtpConfig, subject: &str, body: &str) -> Result { + if !is_configured(smtp) { + return Err("SMTP non configure".into()); + } + + let to_str = smtp.to_emails.join(", "); + + let email = Message::builder() + .from( + smtp.from_email + .parse() + .map_err(|e| format!("Email expediteur invalide: {}", e))?, + ) + .to(to_str + .parse() + .map_err(|e| format!("Email destinataire invalide: {}", e))?) + .subject(subject) + .header(ContentType::TEXT_PLAIN) + .body(body.to_string()) + .map_err(|e| format!("Erreur construction email: {}", e))?; + + let mailer = build_transport(smtp)?; + + mailer + .send(&email) + .map_err(|e| format!("Erreur envoi SMTP: {}", e))?; + + Ok("Email envoye avec succes".into()) +} + +pub fn send_test(smtp: &SmtpConfig) -> Result { + if !is_configured(smtp) { + return Err("Configuration SMTP incomplete".into()); + } + let subject = "[TEST] Supervision - Test de configuration email"; + let body = "Ceci est un email de test.\n\n\ + Si vous recevez ce message, la configuration SMTP est correcte.\n\n\ + -- Supervision"; + send_email(smtp, subject, body) +} + +fn build_transport(smtp: &SmtpConfig) -> Result { + let creds = Credentials::new(smtp.username.clone(), smtp.password.clone()); + + let transport = if smtp.use_tls { + SmtpTransport::starttls_relay(&smtp.server) + .map_err(|e| format!("Erreur connexion SMTP TLS: {}", e))? + .credentials(creds) + .port(smtp.port) + .build() + } else { + SmtpTransport::builder_dangerous(&smtp.server) + .credentials(creds) + .port(smtp.port) + .build() + }; + + Ok(transport) +} + +pub fn format_alert_body(alert: &crate::config::Alert) -> String { + format!( + "Alerte de supervision\n\ + {sep}\n\n\ + Serveur : {host}\n\ + Date : {ts}\n\ + Type : {tp}\n\n\ + Message : {msg}\n\n\ + {sep}\n\ + Supervision - Monitoring automatique", + sep = "=".repeat(40), + host = alert.hostname, + ts = alert.timestamp, + tp = alert.alert_type, + msg = alert.message, + ) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ee17de3 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,238 @@ +use argon2::{Argon2, PasswordHasher, PasswordVerifier}; +use password_hash::{PasswordHash, SaltString}; +use rand::rngs::OsRng; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MAX_ALERTS: usize = 500; + +// --- Structures de configuration --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub secret_key: String, + pub port: u16, + pub check_interval_minutes: u64, + pub alert_cooldown_minutes: u64, + pub thresholds: Thresholds, + pub processes: Vec, + pub smtp: SmtpConfig, + pub admin: AdminConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Thresholds { + pub cpu_percent: u32, + pub ram_percent: u32, + pub disk_percent: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessConfig { + pub name: String, + pub pattern: String, + #[serde(default)] + pub memory_threshold_mb: u64, + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default = "default_true")] + pub alert_on_down: bool, +} + +fn default_true() -> bool { + true +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SmtpConfig { + #[serde(default)] + pub server: String, + #[serde(default = "default_smtp_port")] + pub port: u16, + #[serde(default = "default_true")] + pub use_tls: bool, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password: String, + #[serde(default)] + pub from_email: String, + #[serde(default)] + pub to_emails: Vec, +} + +fn default_smtp_port() -> u16 { + 587 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdminConfig { + pub username: String, + pub password_hash: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Alert { + pub timestamp: String, + #[serde(rename = "type")] + pub alert_type: String, + pub key: String, + pub message: String, + pub value: f64, + pub threshold: f64, + pub hostname: String, +} + +// --- Hashing de mots de passe --- + +pub fn hash_password(password: &str) -> String { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + argon2 + .hash_password(password.as_bytes(), &salt) + .expect("Erreur hashing mot de passe") + .to_string() +} + +pub fn verify_password(password: &str, hash: &str) -> bool { + let parsed = match PasswordHash::new(hash) { + Ok(h) => h, + Err(_) => return false, + }; + Argon2::default() + .verify_password(password.as_bytes(), &parsed) + .is_ok() +} + +pub fn is_default_password(config: &Config) -> bool { + verify_password("admin", &config.admin.password_hash) +} + +// --- Configuration par defaut --- + +pub fn default_config() -> Config { + Config { + secret_key: uuid::Uuid::new_v4().to_string(), + port: 5000, + check_interval_minutes: 1, + alert_cooldown_minutes: 30, + thresholds: Thresholds { + cpu_percent: 90, + ram_percent: 85, + disk_percent: 90, + }, + processes: vec![ + ProcessConfig { + name: "JVM".into(), + pattern: "java".into(), + memory_threshold_mb: 0, + enabled: true, + alert_on_down: true, + }, + ProcessConfig { + name: "Nginx".into(), + pattern: "nginx".into(), + memory_threshold_mb: 0, + enabled: false, + alert_on_down: false, + }, + ProcessConfig { + name: "Amadea Web 8 x64".into(), + pattern: "amadea".into(), + memory_threshold_mb: 0, + enabled: true, + alert_on_down: true, + }, + ], + smtp: SmtpConfig { + server: String::new(), + port: 587, + use_tls: true, + username: String::new(), + password: String::new(), + from_email: String::new(), + to_emails: Vec::new(), + }, + admin: AdminConfig { + username: "admin".into(), + password_hash: hash_password("admin"), + }, + } +} + +// --- Persistence fichier --- + +pub fn config_path(data_dir: &Path) -> PathBuf { + data_dir.join("config.json") +} + +pub fn alerts_path(data_dir: &Path) -> PathBuf { + data_dir.join("alerts.json") +} + +pub fn load_config(data_dir: &Path) -> Config { + fs::create_dir_all(data_dir).ok(); + let path = config_path(data_dir); + + if path.exists() { + let content = fs::read_to_string(&path).unwrap_or_default(); + match serde_json::from_str::(&content) { + Ok(mut config) => { + // Si le hash n'est pas au format argon2, reinitialiser + if !config.admin.password_hash.starts_with("$argon2") { + println!( + "[ATTENTION] Hash de mot de passe incompatible (format Python), \ + reinitialise a 'admin'" + ); + config.admin.password_hash = hash_password("admin"); + save_config(data_dir, &config); + } + config + } + Err(e) => { + eprintln!("[Config] Erreur de lecture: {}. Creation d'une config par defaut.", e); + let config = default_config(); + save_config(data_dir, &config); + config + } + } + } else { + let config = default_config(); + save_config(data_dir, &config); + config + } +} + +pub fn save_config(data_dir: &Path, config: &Config) { + fs::create_dir_all(data_dir).ok(); + let path = config_path(data_dir); + let json = serde_json::to_string_pretty(config).expect("Serialisation config"); + if let Err(e) = fs::write(&path, json) { + eprintln!("[Config] Erreur d'ecriture: {}", e); + } +} + +pub fn load_alerts(data_dir: &Path) -> Vec { + let path = alerts_path(data_dir); + if path.exists() { + let content = fs::read_to_string(&path).unwrap_or_default(); + serde_json::from_str(&content).unwrap_or_default() + } else { + Vec::new() + } +} + +pub fn save_alert(data_dir: &Path, alert: &Alert) { + let mut alerts = load_alerts(data_dir); + alerts.insert(0, alert.clone()); + alerts.truncate(MAX_ALERTS); + let path = alerts_path(data_dir); + let json = serde_json::to_string_pretty(&alerts).unwrap_or_default(); + fs::write(&path, json).ok(); +} + +pub fn clear_alerts(data_dir: &Path) { + let path = alerts_path(data_dir); + fs::write(&path, "[]").ok(); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0937f8c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,922 @@ +mod alerter; +mod config; +mod monitor; + +use axum::body::Body; +use axum::extract::{ConnectInfo, Form, State}; +use axum::http::{HeaderMap, HeaderValue, StatusCode}; +use axum::response::{Html, IntoResponse, Json, Redirect, Response}; +use axum::routing::{get, post}; +use axum::Router; +use config::Alert; +use serde::Deserialize; +use std::collections::{HashMap, VecDeque}; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; +use tera::{Context, Tera}; +use tower_http::services::ServeDir; + +// --- Etat applicatif --- + +pub struct AppState { + pub config: RwLock, + pub metrics: RwLock>, + pub sessions: RwLock>, + pub rate_limiter: RwLock, + pub monitoring_active: AtomicBool, + pub tera: Tera, + pub data_dir: PathBuf, +} + +pub struct Session { + pub username: String, + pub flashes: Vec, +} + +#[derive(Clone, serde::Serialize)] +pub struct FlashMessage { + pub category: String, + pub message: String, +} + +struct SessionInfo { + session_id: String, + #[allow(dead_code)] + username: String, +} + +// --- Rate Limiter --- + +pub struct RateLimiter { + attempts: HashMap>, +} + +impl RateLimiter { + fn new() -> Self { + Self { + attempts: HashMap::new(), + } + } + + fn check(&mut self, ip: IpAddr, max: usize, window: Duration) -> bool { + let now = Instant::now(); + let entry = self.attempts.entry(ip).or_default(); + while let Some(&front) = entry.front() { + if now.duration_since(front) > window { + entry.pop_front(); + } else { + break; + } + } + if entry.len() >= max { + return false; + } + entry.push_back(now); + true + } +} + +// --- Helpers session / auth --- + +fn get_session_id(headers: &HeaderMap) -> Option { + let cookie = headers.get("cookie")?.to_str().ok()?; + for pair in cookie.split(';') { + let pair = pair.trim(); + if let Some(value) = pair.strip_prefix("session_id=") { + return Some(value.to_string()); + } + } + None +} + +fn check_auth(state: &AppState, headers: &HeaderMap) -> Result { + if let Some(sid) = get_session_id(headers) { + let sessions = state.sessions.read().unwrap(); + if let Some(session) = sessions.get(&sid) { + return Ok(SessionInfo { + session_id: sid, + username: session.username.clone(), + }); + } + } + Err(Redirect::to("/login").into_response()) +} + +fn add_flash(state: &AppState, session_id: &str, category: &str, message: &str) { + if let Ok(mut sessions) = state.sessions.write() { + if let Some(session) = sessions.get_mut(session_id) { + session.flashes.push(FlashMessage { + category: category.into(), + message: message.into(), + }); + } + } +} + +fn take_flashes(state: &AppState, session_id: &str) -> Vec { + if let Ok(mut sessions) = state.sessions.write() { + if let Some(session) = sessions.get_mut(session_id) { + return std::mem::take(&mut session.flashes); + } + } + Vec::new() +} + +fn redirect_with_cookie(path: &str, cookie: &str) -> Response { + Response::builder() + .status(StatusCode::SEE_OTHER) + .header("location", path) + .header("set-cookie", cookie) + .body(Body::empty()) + .unwrap() +} + +// --- Contexte template commun --- + +fn base_context(state: &AppState, session: &SessionInfo) -> Context { + let flashes = take_flashes(state, &session.session_id); + let config = state.config.read().unwrap(); + let default_pw = config::is_default_password(&config); + + let mut ctx = Context::new(); + ctx.insert("authenticated", &true); + ctx.insert("flash_messages", &flashes); + ctx.insert("default_pw", &default_pw); + ctx.insert("username", &session.username); + ctx +} + +fn render(tera: &Tera, template: &str, ctx: &Context) -> Result, Response> { + tera.render(template, ctx).map(Html).map_err(|e| { + eprintln!("[Template] Erreur {}: {}", template, e); + (StatusCode::INTERNAL_SERVER_ERROR, "Erreur interne").into_response() + }) +} + +// --- Middleware securite --- + +fn apply_security_headers(response: &mut Response) { + let h = response.headers_mut(); + h.insert("x-content-type-options", HeaderValue::from_static("nosniff")); + h.insert("x-frame-options", HeaderValue::from_static("DENY")); + h.insert( + "x-xss-protection", + HeaderValue::from_static("1; mode=block"), + ); + h.insert( + "referrer-policy", + HeaderValue::from_static("strict-origin-when-cross-origin"), + ); +} + +// --- Routes --- + +// GET /login +async fn login_page(State(state): State>) -> impl IntoResponse { + let mut ctx = Context::new(); + ctx.insert("flash_messages", &Vec::::new()); + let mut resp = render(&state.tera, "login.html", &ctx)?.into_response(); + apply_security_headers(&mut resp); + Ok::<_, Response>(resp) +} + +// POST /login +#[derive(Deserialize)] +struct LoginForm { + username: String, + password: String, +} + +async fn login_action( + State(state): State>, + ConnectInfo(addr): ConnectInfo, + Form(form): Form, +) -> Response { + // Rate limiting : 10 tentatives par minute + { + let mut limiter = state.rate_limiter.write().unwrap(); + if !limiter.check(addr.ip(), 10, Duration::from_secs(60)) { + let mut ctx = Context::new(); + ctx.insert( + "flash_messages", + &vec![FlashMessage { + category: "danger".into(), + message: "Trop de tentatives. Reessayez dans une minute.".into(), + }], + ); + let html = state.tera.render("login.html", &ctx).unwrap_or_default(); + return (StatusCode::TOO_MANY_REQUESTS, Html(html)).into_response(); + } + } + + let config = state.config.read().unwrap().clone(); + let username = form.username.trim(); + + if username == config.admin.username + && config::verify_password(&form.password, &config.admin.password_hash) + { + let session_id = uuid::Uuid::new_v4().to_string(); + { + let mut sessions = state.sessions.write().unwrap(); + sessions.insert( + session_id.clone(), + Session { + username: username.to_string(), + flashes: Vec::new(), + }, + ); + } + let cookie = format!( + "session_id={}; HttpOnly; Path=/; SameSite=Strict; Max-Age=28800", + session_id + ); + redirect_with_cookie("/", &cookie) + } else { + let mut ctx = Context::new(); + ctx.insert( + "flash_messages", + &vec![FlashMessage { + category: "danger".into(), + message: "Identifiants incorrects.".into(), + }], + ); + let html = state.tera.render("login.html", &ctx).unwrap_or_default(); + Html(html).into_response() + } +} + +// GET /logout +async fn logout(State(state): State>, headers: HeaderMap) -> Response { + if let Ok(session) = check_auth(&state, &headers) { + let mut sessions = state.sessions.write().unwrap(); + sessions.remove(&session.session_id); + } + let cookie = "session_id=; HttpOnly; Path=/; Max-Age=0"; + redirect_with_cookie("/login", cookie) +} + +// GET / +async fn dashboard( + State(state): State>, + headers: HeaderMap, +) -> Result { + let session = check_auth(&state, &headers)?; + let mut ctx = base_context(&state, &session); + ctx.insert("active_page", "dashboard"); + + let metrics = state.metrics.read().unwrap().clone(); + ctx.insert("metrics", &metrics); + + let mut resp = render(&state.tera, "dashboard.html", &ctx)?.into_response(); + apply_security_headers(&mut resp); + Ok(resp) +} + +// GET /api/metrics +async fn api_metrics( + State(state): State>, + headers: HeaderMap, +) -> Result { + check_auth(&state, &headers)?; + let metrics = state.metrics.read().unwrap().clone(); + let mut resp = match metrics { + Some(m) => Json(m).into_response(), + None => Json(serde_json::json!({})).into_response(), + }; + apply_security_headers(&mut resp); + Ok(resp) +} + +// POST /api/monitoring/toggle +async fn toggle_monitoring( + State(state): State>, + headers: HeaderMap, +) -> Result { + let session = check_auth(&state, &headers)?; + let was_active = state.monitoring_active.load(Ordering::Relaxed); + state.monitoring_active.store(!was_active, Ordering::Relaxed); + + if was_active { + add_flash(&state, &session.session_id, "warning", "Monitoring arrete."); + } else { + add_flash( + &state, + &session.session_id, + "success", + "Monitoring demarre.", + ); + } + Ok(Redirect::to("/").into_response()) +} + +// GET /settings +async fn settings_page( + State(state): State>, + headers: HeaderMap, +) -> Result { + let session = check_auth(&state, &headers)?; + let mut ctx = base_context(&state, &session); + ctx.insert("active_page", "settings"); + + let config = state.config.read().unwrap().clone(); + ctx.insert("config", &config); + + // SMTP avec mot de passe masque + let password_masked = if config.smtp.password.is_empty() { + String::new() + } else { + "********".into() + }; + let mut smtp = serde_json::to_value(&config.smtp).unwrap(); + smtp["password_masked"] = serde_json::Value::String(password_masked); + ctx.insert("smtp", &smtp); + + let mut resp = render(&state.tera, "settings.html", &ctx)?.into_response(); + apply_security_headers(&mut resp); + Ok(resp) +} + +// POST /settings/thresholds +#[derive(Deserialize)] +struct ThresholdsForm { + cpu_percent: u32, + ram_percent: u32, + disk_percent: u32, +} + +async fn update_thresholds( + State(state): State>, + headers: HeaderMap, + Form(form): Form, +) -> Result { + let session = check_auth(&state, &headers)?; + + for (name, val) in [ + ("cpu_percent", form.cpu_percent), + ("ram_percent", form.ram_percent), + ("disk_percent", form.disk_percent), + ] { + if !(1..=100).contains(&val) { + add_flash( + &state, + &session.session_id, + "danger", + &format!("Le seuil {} doit etre entre 1 et 100.", name), + ); + return Ok(Redirect::to("/settings").into_response()); + } + } + + { + let mut config = state.config.write().unwrap(); + config.thresholds.cpu_percent = form.cpu_percent; + config.thresholds.ram_percent = form.ram_percent; + config.thresholds.disk_percent = form.disk_percent; + config::save_config(&state.data_dir, &config); + } + add_flash(&state, &session.session_id, "success", "Seuils mis a jour."); + Ok(Redirect::to("/settings").into_response()) +} + +// POST /settings/monitoring +#[derive(Deserialize)] +struct MonitoringForm { + check_interval_minutes: u64, + alert_cooldown_minutes: u64, +} + +async fn update_monitoring( + State(state): State>, + headers: HeaderMap, + Form(form): Form, +) -> Result { + let session = check_auth(&state, &headers)?; + + if form.check_interval_minutes < 1 { + add_flash( + &state, + &session.session_id, + "danger", + "L'intervalle doit etre d'au moins 1 minute.", + ); + return Ok(Redirect::to("/settings").into_response()); + } + if form.alert_cooldown_minutes < 1 { + add_flash( + &state, + &session.session_id, + "danger", + "Le cooldown doit etre d'au moins 1 minute.", + ); + return Ok(Redirect::to("/settings").into_response()); + } + + { + let mut config = state.config.write().unwrap(); + config.check_interval_minutes = form.check_interval_minutes; + config.alert_cooldown_minutes = form.alert_cooldown_minutes; + config::save_config(&state.data_dir, &config); + } + add_flash( + &state, + &session.session_id, + "success", + "Parametres de monitoring mis a jour.", + ); + Ok(Redirect::to("/settings").into_response()) +} + +// POST /settings/smtp +#[derive(Deserialize)] +struct SmtpForm { + smtp_server: String, + smtp_port: u16, + smtp_tls: Option, + smtp_username: String, + smtp_password: Option, + smtp_from: String, + smtp_to: String, +} + +async fn update_smtp( + State(state): State>, + headers: HeaderMap, + Form(form): Form, +) -> Result { + let session = check_auth(&state, &headers)?; + + let to_emails: Vec = form + .smtp_to + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + { + let mut config = state.config.write().unwrap(); + config.smtp.server = form.smtp_server.trim().to_string(); + config.smtp.port = form.smtp_port; + config.smtp.use_tls = form.smtp_tls.is_some(); + config.smtp.username = form.smtp_username.trim().to_string(); + config.smtp.from_email = form.smtp_from.trim().to_string(); + config.smtp.to_emails = to_emails; + + if let Some(ref pw) = form.smtp_password { + if !pw.is_empty() { + config.smtp.password = pw.clone(); + } + } + config::save_config(&state.data_dir, &config); + } + add_flash( + &state, + &session.session_id, + "success", + "Configuration SMTP mise a jour.", + ); + Ok(Redirect::to("/settings").into_response()) +} + +// POST /settings/smtp/test +async fn test_smtp( + State(state): State>, + headers: HeaderMap, +) -> Result { + let session = check_auth(&state, &headers)?; + + let smtp_config = state.config.read().unwrap().smtp.clone(); + let result = tokio::task::spawn_blocking(move || alerter::send_test(&smtp_config)).await; + + match result { + Ok(Ok(msg)) => add_flash( + &state, + &session.session_id, + "success", + &format!("Test reussi : {}", msg), + ), + Ok(Err(msg)) => add_flash( + &state, + &session.session_id, + "danger", + &format!("Test echoue : {}", msg), + ), + Err(e) => add_flash( + &state, + &session.session_id, + "danger", + &format!("Erreur: {}", e), + ), + } + Ok(Redirect::to("/settings").into_response()) +} + +// POST /settings/processes +async fn update_processes( + State(state): State>, + headers: HeaderMap, + body: String, +) -> Result { + let session = check_auth(&state, &headers)?; + + let pairs: Vec<(String, String)> = form_urlencoded::parse(body.as_bytes()) + .map(|(k, v)| (k.into_owned(), v.into_owned())) + .collect(); + + let names: Vec<&str> = pairs + .iter() + .filter(|(k, _)| k == "proc_name[]") + .map(|(_, v)| v.as_str()) + .collect(); + let patterns: Vec<&str> = pairs + .iter() + .filter(|(k, _)| k == "proc_pattern[]") + .map(|(_, v)| v.as_str()) + .collect(); + let mem_thresholds: Vec<&str> = pairs + .iter() + .filter(|(k, _)| k == "proc_mem_threshold[]") + .map(|(_, v)| v.as_str()) + .collect(); + let enableds: Vec<&str> = pairs + .iter() + .filter(|(k, _)| k == "proc_enabled[]") + .map(|(_, v)| v.as_str()) + .collect(); + let alert_downs: Vec<&str> = pairs + .iter() + .filter(|(k, _)| k == "proc_alert_down[]") + .map(|(_, v)| v.as_str()) + .collect(); + + let mut processes = Vec::new(); + for (i, name) in names.iter().enumerate() { + let name = name.trim(); + if name.is_empty() { + continue; + } + let pattern = patterns + .get(i) + .map(|s| s.trim().to_lowercase()) + .unwrap_or_default(); + let mem_threshold = mem_thresholds + .get(i) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + let idx_str = i.to_string(); + let enabled = enableds.contains(&idx_str.as_str()); + let alert_on_down = alert_downs.contains(&idx_str.as_str()); + + processes.push(config::ProcessConfig { + name: name.to_string(), + pattern, + memory_threshold_mb: mem_threshold, + enabled, + alert_on_down, + }); + } + + { + let mut config = state.config.write().unwrap(); + config.processes = processes; + config::save_config(&state.data_dir, &config); + } + add_flash( + &state, + &session.session_id, + "success", + "Processus surveilles mis a jour.", + ); + Ok(Redirect::to("/settings").into_response()) +} + +// POST /settings/password +#[derive(Deserialize)] +struct PasswordForm { + current_password: String, + new_password: String, + confirm_password: String, +} + +async fn update_password( + State(state): State>, + headers: HeaderMap, + Form(form): Form, +) -> Result { + let session = check_auth(&state, &headers)?; + + { + let config = state.config.read().unwrap(); + if !config::verify_password(&form.current_password, &config.admin.password_hash) { + add_flash( + &state, + &session.session_id, + "danger", + "Mot de passe actuel incorrect.", + ); + return Ok(Redirect::to("/settings").into_response()); + } + } + + if form.new_password.len() < 8 { + add_flash( + &state, + &session.session_id, + "danger", + "Le nouveau mot de passe doit faire au moins 8 caracteres.", + ); + return Ok(Redirect::to("/settings").into_response()); + } + + if form.new_password != form.confirm_password { + add_flash( + &state, + &session.session_id, + "danger", + "Les mots de passe ne correspondent pas.", + ); + return Ok(Redirect::to("/settings").into_response()); + } + + { + let mut config = state.config.write().unwrap(); + config.admin.password_hash = config::hash_password(&form.new_password); + config::save_config(&state.data_dir, &config); + } + add_flash( + &state, + &session.session_id, + "success", + "Mot de passe mis a jour.", + ); + Ok(Redirect::to("/settings").into_response()) +} + +// POST /settings/port +#[derive(Deserialize)] +struct PortForm { + port: u16, +} + +async fn update_port( + State(state): State>, + headers: HeaderMap, + Form(form): Form, +) -> Result { + let session = check_auth(&state, &headers)?; + + if !(1024..=65535).contains(&form.port) { + add_flash( + &state, + &session.session_id, + "danger", + "Le port doit etre entre 1024 et 65535.", + ); + return Ok(Redirect::to("/settings").into_response()); + } + + { + let mut config = state.config.write().unwrap(); + config.port = form.port; + config::save_config(&state.data_dir, &config); + } + add_flash( + &state, + &session.session_id, + "warning", + &format!( + "Port mis a jour a {}. Redemarrez l'application pour appliquer.", + form.port + ), + ); + Ok(Redirect::to("/settings").into_response()) +} + +// GET /alerts +async fn alerts_page( + State(state): State>, + headers: HeaderMap, +) -> Result { + let session = check_auth(&state, &headers)?; + let mut ctx = base_context(&state, &session); + ctx.insert("active_page", "alerts"); + + let alerts = config::load_alerts(&state.data_dir); + ctx.insert("alerts", &alerts); + + let mut resp = render(&state.tera, "alerts.html", &ctx)?.into_response(); + apply_security_headers(&mut resp); + Ok(resp) +} + +// POST /alerts/clear +async fn clear_alerts( + State(state): State>, + headers: HeaderMap, +) -> Result { + let session = check_auth(&state, &headers)?; + config::clear_alerts(&state.data_dir); + add_flash( + &state, + &session.session_id, + "success", + "Historique des alertes efface.", + ); + Ok(Redirect::to("/alerts").into_response()) +} + +// --- Boucle de monitoring (thread separee) --- + +fn start_monitoring(state: Arc) { + std::thread::spawn(move || { + let mut sys = sysinfo::System::new(); + + // Premiere mesure CPU (besoin de deux lectures) + sys.refresh_cpu_usage(); + std::thread::sleep(Duration::from_secs(1)); + sys.refresh_cpu_usage(); + sys.refresh_memory(); + sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); + let disks = sysinfo::Disks::new_with_refreshed_list(); + + // Collecte initiale + { + let cfg = state.config.read().unwrap().clone(); + let active = state.monitoring_active.load(Ordering::Relaxed); + let metrics = monitor::collect_metrics(&sys, &disks, &cfg, active); + *state.metrics.write().unwrap() = Some(metrics); + } + + let mut last_alerts: HashMap> = HashMap::new(); + + loop { + let is_active = state.monitoring_active.load(Ordering::Relaxed); + + if !is_active { + if let Ok(mut m) = state.metrics.write() { + if let Some(ref mut metrics) = *m { + metrics.monitoring_active = false; + } + } + std::thread::sleep(Duration::from_secs(5)); + continue; + } + + // Rafraichir les metriques systeme + sys.refresh_cpu_usage(); + std::thread::sleep(Duration::from_millis(500)); + sys.refresh_cpu_usage(); + sys.refresh_memory(); + sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); + let disks = sysinfo::Disks::new_with_refreshed_list(); + + let cfg = state.config.read().unwrap().clone(); + let metrics = monitor::collect_metrics(&sys, &disks, &cfg, true); + + // Verification des seuils et envoi d'alertes + let pending = monitor::check_thresholds(&metrics, &cfg); + let cooldown_mins = cfg.alert_cooldown_minutes as i64; + + for alert_info in &pending { + let now = chrono::Local::now(); + let should_alert = match last_alerts.get(&alert_info.key) { + Some(last) => (now - *last).num_minutes() >= cooldown_mins, + None => true, + }; + + if should_alert { + let alert = Alert { + timestamp: now.format("%Y-%m-%dT%H:%M:%S").to_string(), + alert_type: alert_info.alert_type.clone(), + key: alert_info.key.clone(), + message: alert_info.message.clone(), + value: alert_info.value, + threshold: alert_info.threshold, + hostname: metrics.hostname.clone(), + }; + + config::save_alert(&state.data_dir, &alert); + + if alerter::is_configured(&cfg.smtp) { + let subject = + format!("[ALERTE] {} - {}", metrics.hostname, alert_info.message); + let body = alerter::format_alert_body(&alert); + if let Err(e) = alerter::send_email(&cfg.smtp, &subject, &body) { + eprintln!("[Alerter] Erreur envoi: {}", e); + } + } + + last_alerts.insert(alert_info.key.clone(), now); + } + } + + // Mettre a jour les metriques partagees + *state.metrics.write().unwrap() = Some(metrics); + + // Dormir jusqu'au prochain check (verifier toutes les 5s si on doit s'arreter) + let interval = Duration::from_secs(cfg.check_interval_minutes * 60); + let start = Instant::now(); + while start.elapsed() < interval { + if !state.monitoring_active.load(Ordering::Relaxed) { + break; + } + std::thread::sleep(Duration::from_secs(5)); + } + } + }); +} + +// --- Point d'entree --- + +#[tokio::main] +async fn main() { + let exe_dir = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + .unwrap_or_else(|| PathBuf::from(".")); + + // Chercher les templates et static dans le repertoire courant ou celui de l'exe + let work_dir = if PathBuf::from("templates").exists() { + PathBuf::from(".") + } else { + exe_dir + }; + + let data_dir = work_dir.join("data"); + let config = config::load_config(&data_dir); + let port = config.port; + + // Charger les templates Tera + let template_path = work_dir.join("templates").join("**").join("*.html"); + let tera = match Tera::new(template_path.to_str().unwrap()) { + Ok(t) => t, + Err(e) => { + eprintln!("[ERREUR] Chargement des templates: {}", e); + std::process::exit(1); + } + }; + + if config::is_default_password(&config) { + println!( + "[ATTENTION] Le mot de passe admin est encore 'admin'. Changez-le immediatement !" + ); + } + + let state = Arc::new(AppState { + config: RwLock::new(config), + metrics: RwLock::new(None), + sessions: RwLock::new(HashMap::new()), + rate_limiter: RwLock::new(RateLimiter::new()), + monitoring_active: AtomicBool::new(true), + tera, + data_dir, + }); + + // Demarrer la boucle de monitoring + start_monitoring(Arc::clone(&state)); + println!("[Supervision] Monitoring actif"); + + // Toutes les routes + let static_dir = work_dir.join("static"); + let app = Router::new() + .route("/login", get(login_page).post(login_action)) + .route("/", get(dashboard)) + .route("/logout", get(logout)) + .route("/api/metrics", get(api_metrics)) + .route("/api/monitoring/toggle", post(toggle_monitoring)) + .route("/settings", get(settings_page)) + .route("/settings/thresholds", post(update_thresholds)) + .route("/settings/monitoring", post(update_monitoring)) + .route("/settings/smtp", post(update_smtp)) + .route("/settings/smtp/test", post(test_smtp)) + .route("/settings/processes", post(update_processes)) + .route("/settings/password", post(update_password)) + .route("/settings/port", post(update_port)) + .route("/alerts", get(alerts_page)) + .route("/alerts/clear", post(clear_alerts)) + .nest_service("/static", ServeDir::new(static_dir)) + .with_state(state); + + // Demarrage du serveur + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + println!("[Supervision] Demarrage sur le port {}", port); + println!("[Supervision] Interface : http://localhost:{}", port); + + let listener = match tokio::net::TcpListener::bind(addr).await { + Ok(l) => l, + Err(_) => { + eprintln!("[ERREUR] Le port {} est deja utilise.", port); + eprintln!("Modifiez le port dans data/config.json ou liberez le port."); + std::process::exit(1); + } + }; + + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); +} diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..a688135 --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,344 @@ +use crate::config::{Config, ProcessConfig}; +use serde::Serialize; +use sysinfo::{Disks, System}; + +// --- Structures de metriques --- + +#[derive(Debug, Clone, Serialize)] +pub struct Metrics { + pub timestamp: String, + pub hostname: String, + pub os: String, + pub cpu: CpuMetrics, + pub ram: RamMetrics, + pub disks: Vec, + pub processes: Vec, + pub uptime: String, + pub boot_time: String, + pub monitoring_active: bool, + pub last_check: String, + pub next_check: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CpuMetrics { + pub percent: f32, + pub cores: usize, + pub threshold: u32, + pub status: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RamMetrics { + pub percent: f64, + pub total_gb: f64, + pub used_gb: f64, + pub available_gb: f64, + pub threshold: u32, + pub status: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DiskMetrics { + pub drive: String, + pub mountpoint: String, + pub percent: f64, + pub total_gb: f64, + pub used_gb: f64, + pub free_gb: f64, + pub threshold: u32, + pub status: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ProcessMetrics { + pub name: String, + pub pattern: String, + pub running: bool, + pub enabled: bool, + pub alert_on_down: bool, + pub instance_count: usize, + pub total_memory_mb: f64, + pub total_cpu_percent: f32, + pub memory_threshold_mb: u64, + pub memory_status: String, + pub pids: Vec, +} + +// Info d'alerte retournee par check_thresholds +pub struct AlertInfo { + pub key: String, + pub alert_type: String, + pub message: String, + pub value: f64, + pub threshold: f64, +} + +// --- Collecte des metriques --- + +pub fn collect_metrics(sys: &System, disks: &Disks, config: &Config, monitoring_active: bool) -> Metrics { + let now = chrono::Local::now(); + let interval_mins = config.check_interval_minutes; + + // CPU + let cpu_percent = sys.global_cpu_usage(); + let cpu_status = eval_status(cpu_percent as f64, config.thresholds.cpu_percent as f64); + + // RAM + let total_mem = sys.total_memory() as f64; + let used_mem = sys.used_memory() as f64; + let available_mem = sys.available_memory() as f64; + let ram_percent = if total_mem > 0.0 { + (used_mem / total_mem) * 100.0 + } else { + 0.0 + }; + let ram_status = eval_status(ram_percent, config.thresholds.ram_percent as f64); + + // Disques + let gb = 1024.0 * 1024.0 * 1024.0; + let ignored_fs = ["squashfs", "tmpfs", "devtmpfs", "overlay", "iso9660"]; + let disk_metrics: Vec = disks + .list() + .iter() + .filter(|d| { + let fs = d.file_system().to_string_lossy().to_string(); + let name = d.name().to_string_lossy().to_string(); + !ignored_fs.contains(&fs.as_str()) + && !name.starts_with("/dev/loop") + && d.total_space() >= (1024 * 1024 * 1024) // >= 1 Go + }) + .filter_map(|d| { + let total = d.total_space() as f64; + let available = d.available_space() as f64; + let used = total - available; + let percent = if total > 0.0 { + (used / total) * 100.0 + } else { + return None; + }; + let status = eval_status(percent, config.thresholds.disk_percent as f64); + Some(DiskMetrics { + drive: d.name().to_string_lossy().to_string(), + mountpoint: d.mount_point().to_string_lossy().to_string(), + percent: round1(percent), + total_gb: round1(total / gb), + used_gb: round1(used / gb), + free_gb: round1(available / gb), + threshold: config.thresholds.disk_percent, + status, + }) + }) + .collect(); + + // Processus surveilles + let process_metrics = check_processes(sys, &config.processes); + + // Uptime + let boot_secs = System::boot_time(); + let boot_time = chrono::DateTime::from_timestamp(boot_secs as i64, 0) + .unwrap_or_default() + .with_timezone(&chrono::Local); + let uptime_secs = System::uptime(); + let uptime_str = format_duration(uptime_secs); + + Metrics { + timestamp: now.format("%Y-%m-%dT%H:%M:%S").to_string(), + hostname: System::host_name().unwrap_or_else(|| "inconnu".into()), + os: format!( + "{} {}", + System::name().unwrap_or_default(), + System::os_version().unwrap_or_default() + ), + cpu: CpuMetrics { + percent: (cpu_percent * 10.0).round() / 10.0, + cores: sys.cpus().len(), + threshold: config.thresholds.cpu_percent, + status: cpu_status, + }, + ram: RamMetrics { + percent: round1(ram_percent), + total_gb: round1(total_mem / gb), + used_gb: round1(used_mem / gb), + available_gb: round1(available_mem / gb), + threshold: config.thresholds.ram_percent, + status: ram_status, + }, + disks: disk_metrics, + processes: process_metrics, + uptime: uptime_str, + boot_time: boot_time.format("%Y-%m-%dT%H:%M:%S").to_string(), + monitoring_active, + last_check: now.format("%Y-%m-%dT%H:%M:%S").to_string(), + next_check: (now + chrono::Duration::minutes(interval_mins as i64)) + .format("%Y-%m-%dT%H:%M:%S") + .to_string(), + } +} + +fn check_processes(sys: &System, process_configs: &[ProcessConfig]) -> Vec { + process_configs + .iter() + .map(|cfg| { + let pattern = cfg.pattern.to_lowercase(); + let mut found_pids = Vec::new(); + let mut total_mem: f64 = 0.0; + let mut total_cpu: f32 = 0.0; + + if cfg.enabled { + for (pid, proc) in sys.processes() { + let pname = proc.name().to_string_lossy().to_lowercase(); + let cmdline = proc + .cmd() + .iter() + .map(|s| s.to_string_lossy().to_lowercase()) + .collect::>() + .join(" "); + + if pname.contains(&pattern) || cmdline.contains(&pattern) { + let mem_mb = proc.memory() as f64 / (1024.0 * 1024.0); + total_mem += mem_mb; + total_cpu += proc.cpu_usage(); + found_pids.push(pid.as_u32()); + } + } + } + + let running = !found_pids.is_empty(); + let mem_status = if cfg.memory_threshold_mb > 0 && total_mem > 0.0 { + eval_status(total_mem, cfg.memory_threshold_mb as f64) + } else { + "ok".into() + }; + + ProcessMetrics { + name: cfg.name.clone(), + pattern: cfg.pattern.clone(), + running, + enabled: cfg.enabled, + alert_on_down: cfg.alert_on_down, + instance_count: found_pids.len(), + total_memory_mb: round1(total_mem), + total_cpu_percent: (total_cpu * 10.0).round() / 10.0, + memory_threshold_mb: cfg.memory_threshold_mb, + memory_status: mem_status, + pids: found_pids, + } + }) + .collect() +} + +// --- Verification des seuils --- + +pub fn check_thresholds(metrics: &Metrics, _config: &Config) -> Vec { + let mut alerts = Vec::new(); + + // CPU + if metrics.cpu.status == "critical" { + alerts.push(AlertInfo { + key: "cpu".into(), + alert_type: "threshold".into(), + message: format!( + "CPU a {}% (seuil: {}%)", + metrics.cpu.percent, metrics.cpu.threshold + ), + value: metrics.cpu.percent as f64, + threshold: metrics.cpu.threshold as f64, + }); + } + + // RAM + if metrics.ram.status == "critical" { + alerts.push(AlertInfo { + key: "ram".into(), + alert_type: "threshold".into(), + message: format!( + "RAM a {}% (seuil: {}%)", + metrics.ram.percent, metrics.ram.threshold + ), + value: metrics.ram.percent, + threshold: metrics.ram.threshold as f64, + }); + } + + // Disques + for disk in &metrics.disks { + if disk.status == "critical" { + alerts.push(AlertInfo { + key: format!("disk_{}", disk.drive), + alert_type: "threshold".into(), + message: format!( + "Disque {} a {}% (seuil: {}%)", + disk.drive, disk.percent, disk.threshold + ), + value: disk.percent, + threshold: disk.threshold as f64, + }); + } + } + + // Processus + for proc in &metrics.processes { + if !proc.enabled { + continue; + } + if proc.alert_on_down && !proc.running { + alerts.push(AlertInfo { + key: format!("process_down_{}", proc.name), + alert_type: "process_down".into(), + message: format!( + "Processus '{}' non detecte (pattern: {})", + proc.name, proc.pattern + ), + value: 0.0, + threshold: 0.0, + }); + } + if proc.memory_threshold_mb > 0 && proc.memory_status == "critical" { + alerts.push(AlertInfo { + key: format!("process_mem_{}", proc.name), + alert_type: "threshold".into(), + message: format!( + "Processus '{}' utilise {} Mo (seuil: {} Mo)", + proc.name, proc.total_memory_mb, proc.memory_threshold_mb + ), + value: proc.total_memory_mb, + threshold: proc.memory_threshold_mb as f64, + }); + } + } + + alerts +} + +// --- Utilitaires --- + +fn eval_status(value: f64, threshold: f64) -> String { + if threshold <= 0.0 { + return "ok".into(); + } + let ratio = value / threshold; + if ratio >= 1.0 { + "critical".into() + } else if ratio >= 0.80 { + "warning".into() + } else { + "ok".into() + } +} + +fn round1(v: f64) -> f64 { + (v * 10.0).round() / 10.0 +} + +fn format_duration(secs: u64) -> String { + let days = secs / 86400; + let hours = (secs % 86400) / 3600; + let mins = (secs % 3600) / 60; + let s = secs % 60; + if days > 0 { + format!("{} jour(s), {:02}:{:02}:{:02}", days, hours, mins, s) + } else { + format!("{:02}:{:02}:{:02}", hours, mins, s) + } +} diff --git a/templates/alerts.html b/templates/alerts.html index fcaef00..942e2ff 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -1,11 +1,11 @@ {% extends "base.html" %} -{% block title %}Supervision - Alertes{% endblock %} +{% block title %}Supervision - Alertes{% endblock title %} {% block content %}

Historique des alertes

{% if alerts %} -
+
{% endif %} -{% endblock %} +{% endblock content %} diff --git a/templates/base.html b/templates/base.html index e4715cd..6e87cfc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,16 +3,16 @@ - {% block title %}Supervision{% endblock %} + {% block title %}Supervision{% endblock title %} - + - {% if current_user.is_authenticated %} + {% if authenticated %}