summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.formatter.exs5
-rw-r--r--.gitignore34
-rw-r--r--Cargo.lock3439
-rw-r--r--Cargo.toml36
-rw-r--r--Dockerfile84
-rw-r--r--config/config.exs43
-rw-r--r--config/dev.exs76
-rw-r--r--config/prod.exs14
-rw-r--r--config/runtime.exs196
-rw-r--r--config/test.exs26
-rw-r--r--docker-compose.yml57
-rw-r--r--installation/docker-compose.deploy.yml67
-rw-r--r--installation/setup_db.sql19
-rw-r--r--installation/silmataivas.nginx81
-rw-r--r--installation/silmataivas.service48
-rw-r--r--lefthook.yml3
-rw-r--r--lib/mix/tasks/silmataivas.user.new.ex48
-rw-r--r--lib/silmataivas.ex9
-rw-r--r--lib/silmataivas/application.ex37
-rw-r--r--lib/silmataivas/locations.ex104
-rw-r--r--lib/silmataivas/locations/location.ex19
-rw-r--r--lib/silmataivas/mailer.ex44
-rw-r--r--lib/silmataivas/ntfy_notifier.ex35
-rw-r--r--lib/silmataivas/release.ex136
-rw-r--r--lib/silmataivas/repo.ex20
-rw-r--r--lib/silmataivas/scheduler.ex4
-rw-r--r--lib/silmataivas/users.ex124
-rw-r--r--lib/silmataivas/users/user.ex29
-rw-r--r--lib/silmataivas/weather_poller.ex102
-rw-r--r--lib/silmataivas_web.ex67
-rw-r--r--lib/silmataivas_web/controllers/changeset_json.ex25
-rw-r--r--lib/silmataivas_web/controllers/error_json.ex21
-rw-r--r--lib/silmataivas_web/controllers/fallback_controller.ex24
-rw-r--r--lib/silmataivas_web/controllers/health_controller.ex9
-rw-r--r--lib/silmataivas_web/controllers/location_controller.ex46
-rw-r--r--lib/silmataivas_web/controllers/location_json.ex25
-rw-r--r--lib/silmataivas_web/endpoint.ex51
-rw-r--r--lib/silmataivas_web/gettext.ex25
-rw-r--r--lib/silmataivas_web/plugs/admin_only.ex8
-rw-r--r--lib/silmataivas_web/plugs/auth.ex20
-rw-r--r--lib/silmataivas_web/router.ex41
-rw-r--r--lib/silmataivas_web/telemetry.ex93
-rw-r--r--migrations/20240101000000_create_users.sql6
-rw-r--r--migrations/20240101000100_create_locations.sql9
-rw-r--r--migrations/20240101000200_create_weather_thresholds.sql13
-rw-r--r--migrations/20240101000300_create_user_notification_settings.sql32
-rw-r--r--mix.exs77
-rw-r--r--mix.lock63
-rw-r--r--priv/gettext/en/LC_MESSAGES/errors.po112
-rw-r--r--priv/gettext/errors.pot109
-rw-r--r--priv/repo/migrations/.formatter.exs4
-rw-r--r--priv/repo/migrations/20250323093704_create_users.exs13
-rw-r--r--priv/repo/migrations/20250323093713_create_locations.exs15
-rw-r--r--priv/repo/migrations/20250326104054_add_role_to_users.exs9
-rw-r--r--priv/repo/seeds.exs11
-rw-r--r--priv/static/favicon.icobin152 -> 0 bytes
-rw-r--r--priv/static/robots.txt5
-rw-r--r--src/health.rs6
-rw-r--r--src/lib.rs31
-rw-r--r--src/locations.rs139
-rw-r--r--src/notifications.rs294
-rw-r--r--src/users.rs139
-rw-r--r--src/weather_poller.rs192
-rw-r--r--src/weather_thresholds.rs165
-rw-r--r--test/silmataivas/locations_test.exs127
-rw-r--r--test/silmataivas/users_test.exs62
-rw-r--r--test/silmataivas_web/controllers/error_json_test.exs12
-rw-r--r--test/silmataivas_web/controllers/health_controller_test.exs8
-rw-r--r--test/silmataivas_web/controllers/location_controller_test.exs203
-rw-r--r--test/silmataivas_web/controllers/location_json_test.exs48
-rw-r--r--test/silmataivas_web/plugs/admin_only_test.exs49
-rw-r--r--test/silmataivas_web/plugs/auth_test.exs60
-rw-r--r--test/support/conn_case.ex38
-rw-r--r--test/support/data_case.ex58
-rw-r--r--test/support/fixtures/locations_fixtures.ex69
-rw-r--r--test/support/fixtures/users_fixtures.ex41
-rw-r--r--test/test_helper.exs4
77 files changed, 4511 insertions, 3106 deletions
diff --git a/.formatter.exs b/.formatter.exs
deleted file mode 100644
index 5971023..0000000
--- a/.formatter.exs
+++ /dev/null
@@ -1,5 +0,0 @@
-[
- import_deps: [:ecto, :ecto_sql, :phoenix],
- subdirectories: ["priv/*/migrations"],
- inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}", "priv/*/seeds.exs"]
-]
diff --git a/.gitignore b/.gitignore
index 4d5bcdc..1c01446 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,31 +1,17 @@
-# The directory Mix will write compiled artifacts to.
-/_build/
+### Rust ###
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
-# If you run "mix test --cover", coverage assets end up here.
-/cover/
+# These are backup files generated by rustfmt
+**/*.rs.bk
-# The directory Mix downloads your dependencies sources to.
-/deps/
-
-# Where 3rd-party dependencies like ExDoc output generated docs.
-/doc/
-
-# Ignore .fetch files in case you like to edit your project deps locally.
-/.fetch
-
-# If the VM crashes, it generates a dump, let's ignore it too.
-erl_crash.dump
-
-# Also ignore archive artifacts (built via "mix archive.build").
-*.ez
-
-# Temporary files, for example, from tests.
-/tmp/
-
-# Ignore package tarball (built via "mix hex.build").
-silmataivas-*.tar
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
.env
VERSION
.npmrc
node_modules/
+.cursor/
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..26fa8b5
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,3439 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
+[[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.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "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",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+dependencies = [
+ "serde",
+]
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "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 = "chumsky"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
+dependencies = [
+ "hashbrown 0.14.5",
+ "stacker",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crossbeam-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-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[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",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+dependencies = [
+ "serde",
+]
+
+[[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 = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[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.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "globset"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
+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 = "h2"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.4",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "hostname"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-link",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "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 = "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.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.4",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "lettre"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb2a0354e9ece2fcdcf9fa53417f6de587230c0c248068eb058fa26c4a753179"
+dependencies = [
+ "base64",
+ "chumsky",
+ "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.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+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.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "parse-zoneinfo"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
+dependencies = [
+ "regex",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pest"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
+dependencies = [
+ "memchr",
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
+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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+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 = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "psm"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quoted_printable"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[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.16",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "reqwest"
+version = "0.12.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rusqlite"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink 0.9.1",
+ "libsqlite3-sys",
+ "serde_json",
+ "smallvec",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[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.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "silmataivas"
+version = "0.2.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "chrono",
+ "dotenv",
+ "hyper",
+ "lettre",
+ "reqwest",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sqlx",
+ "tera",
+ "tokio",
+ "tokio-task-scheduler",
+ "tower",
+ "tower-http",
+ "uuid",
+]
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[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"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
+dependencies = [
+ "base64",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.4",
+ "hashlink 0.10.0",
+ "indexmap",
+ "log",
+ "memchr",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "thiserror",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "stacker"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+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"
+dependencies = [
+ "futures-core",
+]
+
+[[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 = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tera"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding",
+ "pest",
+ "pest_derive",
+ "rand",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "unic-segment",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.46.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-task-scheduler"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41628a68557309a10fcd62e96070775df27ca5415b2d914ac5774be105978b8"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "chrono",
+ "futures",
+ "once_cell",
+ "rusqlite",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "uuid",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
+dependencies = [
+ "unic-ucd-segment",
+]
+
+[[package]]
+name = "unic-ucd-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[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.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "whoami"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
+dependencies = [
+ "redox_syscall",
+ "wasite",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..b7471d9
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,36 @@
+[package]
+name = "silmataivas"
+version = "0.2.0"
+edition = "2024"
+
+[dependencies]
+axum = "0.8.4"
+chrono = "0.4.41"
+hyper = "1.6.0"
+lettre = "0.11.17"
+reqwest = { version = "0.12.22", features = ["json"] }
+serde = "1.0.219"
+serde_json = "1.0.140"
+sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-native-tls"] }
+tera = "1.20.0"
+tokio = "1.46.1"
+tokio-task-scheduler = "1.0.0"
+tower = "0.5.2"
+tower-http = "0.6.6"
+uuid = "1.17.0"
+
+[dev-dependencies]
+anyhow = "1.0.98"
+axum = "0.8.4"
+dotenv = "0.15.0"
+lettre = "0.11.17"
+reqwest = "0.12.22"
+serde = "1.0.219"
+serde_derive = "1.0.219"
+serde_json = "1.0.140"
+sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-native-tls"] }
+tera = "1.20.0"
+tokio = "1.46.1"
+tokio-task-scheduler = "1.0.0"
+tower-http = "0.6.6"
+uuid = "1.17.0"
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index d54e6e9..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,84 +0,0 @@
-# Stage 1: Build the application
-FROM hexpm/elixir:1.18.3-erlang-25.0.4-debian-bookworm-20250317-slim AS build
-
-# Install build dependencies
-RUN apt-get update -y && \
- apt-get install -y --no-install-recommends build-essential git && \
- apt-get clean && \
- rm -rf /var/lib/apt/lists/*
-
-# Set environment variables
-ENV MIX_ENV=prod
-
-# Prepare build directory
-WORKDIR /app
-
-# Install hex and rebar
-RUN mix local.hex --force && \
- mix local.rebar --force
-
-# Copy configuration files first to cache dependencies
-COPY mix.exs mix.lock ./
-COPY config config
-
-# Get dependencies
-RUN mix deps.get --only prod
-
-# Copy the rest of the application code
-COPY lib lib
-COPY priv priv
-# No rel directory yet
-# COPY rel rel
-
-# Compile the application and create release
-RUN mix deps.compile
-RUN mix compile
-RUN mix release
-
-# Stage 2: Create the minimal runtime image
-FROM debian:bookworm-slim AS app
-
-# Install runtime dependencies
-RUN apt-get update -y && \
- apt-get install -y --no-install-recommends \
- libstdc++6 \
- openssl \
- ca-certificates \
- ncurses-bin \
- sqlite3 \
- libsqlite3-dev \
- curl && \
- apt-get clean && \
- rm -rf /var/lib/apt/lists/*
-
-# Set environment variables
-ENV LANG=C.UTF-8 \
- PHX_SERVER=true \
- DB_ADAPTER=sqlite \
- DATABASE_URL=sqlite3:/app/data/silmataivas.db
-
-WORKDIR /app
-
-# Copy the release from the build stage
-COPY --from=build /app/_build/prod/rel/silmataivas ./
-
-# Create data directory and non-root user with proper permissions
-RUN mkdir -p /app/data && \
- useradd -m silmataivas && \
- chown -R silmataivas:silmataivas /app && \
- chmod -R 750 /app/data
-
-USER silmataivas
-
-# Document which ports the application uses
-EXPOSE 4000
-
-# Define volumes for persistence
-VOLUME ["/app/data"]
-
-# Add health check
-HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
- CMD curl -f http://localhost:4000/health || exit 1
-
-# Set the command to start the app
-CMD ["/app/bin/silmataivas", "start"] \ No newline at end of file
diff --git a/config/config.exs b/config/config.exs
deleted file mode 100644
index c3d80d5..0000000
--- a/config/config.exs
+++ /dev/null
@@ -1,43 +0,0 @@
-# This file is responsible for configuring your application
-# and its dependencies with the aid of the Config module.
-#
-# This configuration file is loaded before any dependency and
-# is restricted to this project.
-
-# General application configuration
-import Config
-
-config :silmataivas,
- ecto_repos: [Silmataivas.Repo],
- generators: [timestamp_type: :utc_datetime]
-
-# Configures the endpoint
-config :silmataivas, SilmataivasWeb.Endpoint,
- url: [host: "localhost"],
- adapter: Bandit.PhoenixAdapter,
- render_errors: [
- formats: [json: SilmataivasWeb.ErrorJSON],
- layout: false
- ],
- pubsub_server: Silmataivas.PubSub,
- live_view: [signing_salt: "uNqy5+eV"]
-
-# Configures the mailer
-#
-# By default it uses the "Local" adapter which stores the emails
-# locally. You can see the emails in your browser, at "/dev/mailbox".
-#
-# For production it's recommended to configure a different adapter
-# at the `config/runtime.exs`.
-
-# Configures Elixir's Logger
-config :logger, :console,
- format: "$time $metadata[$level] $message\n",
- metadata: [:request_id]
-
-# Use Jason for JSON parsing in Phoenix
-config :phoenix, :json_library, Jason
-
-# Import environment specific config. This must remain at the bottom
-# of this file so it overrides the configuration defined above.
-import_config "#{config_env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
deleted file mode 100644
index 2c2821e..0000000
--- a/config/dev.exs
+++ /dev/null
@@ -1,76 +0,0 @@
-import Config
-
-# Database configuration is now handled in runtime.exs
-# using DB_ADAPTER and DATABASE_URL environment variables
-config :silmataivas, Silmataivas.Repo, show_sensitive_data_on_connection_error: true
-
-# OpenWeatherMap API configuration for development
-config :silmataivas, :openweathermap_api_key, System.get_env("OPENWEATHERMAP_API_KEY")
-
-# For development, we disable any cache and enable
-# debugging and code reloading.
-#
-# The watchers configuration can be used to run external
-# watchers to your application. For example, we can use it
-# to bundle .js and .css sources.
-# Binding to loopback ipv4 address prevents access from other machines.
-config :silmataivas, SilmataivasWeb.Endpoint,
- # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
- http: [ip: {127, 0, 0, 1}, port: 4000],
- check_origin: false,
- code_reloader: true,
- debug_errors: true,
- secret_key_base: "2n9j4hR64BSG/CPD59PFf9u2obxbBtOJw2KMeJmXdQDwt4zOFyyIwWX7lamgQJhi",
- watchers: []
-
-# ## SSL Support
-#
-# In order to use HTTPS in development, a self-signed
-# certificate can be generated by running the following
-# Mix task:
-#
-# mix phx.gen.cert
-#
-# Run `mix help phx.gen.cert` for more information.
-#
-# The `http:` config above can be replaced with:
-#
-# https: [
-# port: 4001,
-# cipher_suite: :strong,
-# keyfile: "priv/cert/selfsigned_key.pem",
-# certfile: "priv/cert/selfsigned.pem"
-# ],
-#
-# If desired, both `http:` and `https:` keys can be
-# configured to run both http and https servers on
-# different ports.
-
-# Enable dev routes for dashboard and mailbox
-config :silmataivas, dev_routes: true
-
-# Do not include metadata nor timestamps in development logs
-config :logger, :console, format: "[$level] $message\n"
-
-# Set a higher stacktrace during development. Avoid configuring such
-# in production as building large stacktraces may be expensive.
-config :phoenix, :stacktrace_depth, 20
-
-# Initialize plugs at runtime for faster development compilation
-config :phoenix, :plug_init_mode, :runtime
-
-# Disable swoosh api client as it is only required for production adapters.
-config :swoosh, :api_client, Swoosh.ApiClient.Hackney
-
-config :silmataivas, Silmataivas.Mailer,
- adapter: Swoosh.Adapters.AmazonSES,
- access_key: System.get_env("AWS_ACCESS_KEY_ID"),
- secret: System.get_env("AWS_SECRET_ACCESS_KEY"),
- region: "eu-central-1"
-
-# Configure scheuler
-config :silmataivas, Silmataivas.Scheduler,
- jobs: [
- # {"0 * * * *", {Silmataivas.WeatherPoller, :check_all, []}}
- {"*/5 * * * *", {Silmataivas.WeatherPoller, :check_all, []}}
- ]
diff --git a/config/prod.exs b/config/prod.exs
deleted file mode 100644
index 07964dd..0000000
--- a/config/prod.exs
+++ /dev/null
@@ -1,14 +0,0 @@
-import Config
-
-# Configures Swoosh API Client
-# config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Silmataivas.Finch
-config :swoosh, :api_client, Swoosh.ApiClient.Hackney
-
-# Disable Swoosh Local Memory Storage
-config :swoosh, local: false
-
-# Do not print debug messages in production
-# config :logger, level: :info
-
-# Runtime production configuration, including reading
-# of environment variables, is done on config/runtime.exs.
diff --git a/config/runtime.exs b/config/runtime.exs
deleted file mode 100644
index a038e6f..0000000
--- a/config/runtime.exs
+++ /dev/null
@@ -1,196 +0,0 @@
-import Config
-
-# config/runtime.exs is executed for all environments, including
-# during releases. It is executed after compilation and before the
-# system starts, so it is typically used to load production configuration
-# and secrets from environment variables or elsewhere. Do not define
-# any compile-time configuration in here, as it won't be applied.
-# The block below contains prod specific runtime configuration.
-
-# ## Using releases
-#
-# If you use `mix release`, you need to explicitly enable the server
-# by passing the PHX_SERVER=true when you start it:
-#
-# PHX_SERVER=true bin/silmataivas start
-#
-# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
-# script that automatically sets the env var above.
-if System.get_env("PHX_SERVER") do
- config :silmataivas, SilmataivasWeb.Endpoint, server: true
-end
-
-# Configure database adapter (SQLite or PostgreSQL)
-db_adapter = System.get_env("DB_ADAPTER", "sqlite")
-
-# In test environment, configure test database with sandbox pool
-if config_env() == :test do
- database_path = System.get_env("DATABASE_URL", "sqlite3:/tmp/silmataivas_test.db")
-
- config :silmataivas, Silmataivas.Repo,
- adapter: Ecto.Adapters.SQLite3,
- database: String.replace_prefix(database_path, "sqlite3:", ""),
- pool: Ecto.Adapters.SQL.Sandbox,
- pool_size: System.schedulers_online() * 2,
- queue_target: 5000,
- queue_interval: 10000,
- timeout: 30000,
- pragma: [
- # Write-Ahead Logging for better concurrency
- journal_mode: :wal,
- # Wait longer before failing on locks
- busy_timeout: 10000,
- # Balance between safety and performance
- synchronous: :normal
- ]
-else
- case db_adapter do
- "sqlite" ->
- database_path =
- System.get_env(
- "DATABASE_URL",
- "sqlite3:#{Path.join(System.get_env("HOME"), ".silmataivas.db")}"
- )
-
- config :silmataivas, Silmataivas.Repo,
- adapter: Ecto.Adapters.SQLite3,
- database: String.replace_prefix(database_path, "sqlite3:", ""),
- pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
-
- "postgres" ->
- database_url = System.get_env("DATABASE_URL")
-
- if config_env() != :prod and is_nil(database_url) do
- # Default development PostgreSQL config if DATABASE_URL is not set
- config :silmataivas, Silmataivas.Repo,
- adapter: Ecto.Adapters.Postgres,
- username: System.get_env("PGUSER", "postgres"),
- password: System.get_env("PGPASSWORD", "postgres"),
- hostname: System.get_env("PGHOST", "localhost"),
- database: System.get_env("PGDATABASE", "silmataivas_#{config_env()}"),
- pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
- else
- maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
-
- if config_env() == :prod and is_nil(database_url) do
- raise """
- environment variable DATABASE_URL is missing.
- For example: ecto://USER:PASS@HOST/DATABASE
- """
- end
-
- config :silmataivas, Silmataivas.Repo,
- adapter: Ecto.Adapters.Postgres,
- url: database_url,
- pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
- socket_options: maybe_ipv6
- end
-
- other ->
- raise "Unsupported database adapter: #{other}. Supported adapters are 'sqlite' and 'postgres'."
- end
-end
-
-if config_env() == :prod do
- # Add OpenWeatherMap API key for production
- openweathermap_api_key =
- System.get_env("OPENWEATHERMAP_API_KEY") ||
- raise """
- environment variable OPENWEATHERMAP_API_KEY is missing.
- Please set this environment variable to your OpenWeatherMap API key.
- """
-
- config :silmataivas, :openweathermap_api_key, openweathermap_api_key
-
- # The secret key base is used to sign/encrypt cookies and other secrets.
- # A default value is used in config/dev.exs and config/test.exs but you
- # want to use a different value for prod and you most likely don't want
- # to check this value into version control, so we use an environment
- # variable instead.
- secret_key_base =
- System.get_env("SECRET_KEY_BASE") ||
- raise """
- environment variable SECRET_KEY_BASE is missing.
- You can generate one by calling: mix phx.gen.secret
- """
-
- host = System.get_env("PHX_HOST") || "example.com"
- port = String.to_integer(System.get_env("PORT") || "4000")
-
- config :silmataivas, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
-
- config :silmataivas, SilmataivasWeb.Endpoint,
- url: [host: host, port: 443, scheme: "https"],
- http: [
- # Enable IPv6 and bind on all interfaces.
- # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
- # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
- # for details about using IPv6 vs IPv4 and loopback vs public addresses.
- ip: {0, 0, 0, 0},
- port: port
- ],
- secret_key_base: secret_key_base
-
- config :logger, level: String.to_atom(System.get_env("LOG_LEVEL") || "info")
-
- # ## SSL Support
- #
- # To get SSL working, you will need to add the `https` key
- # to your endpoint configuration:
- #
- # config :silmataivas, SilmataivasWeb.Endpoint,
- # https: [
- # ...,
- # port: 443,
- # cipher_suite: :strong,
- # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
- # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
- # ]
- #
- # The `cipher_suite` is set to `:strong` to support only the
- # latest and more secure SSL ciphers. This means old browsers
- # and clients may not be supported. You can set it to
- # `:compatible` for wider support.
- #
- # `:keyfile` and `:certfile` expect an absolute path to the key
- # and cert in disk or a relative path inside priv, for example
- # "priv/ssl/server.key". For all supported SSL configuration
- # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
- #
- # We also recommend setting `force_ssl` in your config/prod.exs,
- # ensuring no data is ever sent via http, always redirecting to https:
- #
- # config :silmataivas, SilmataivasWeb.Endpoint,
- # force_ssl: [hsts: true]
- #
- # Check `Plug.SSL` for all available options in `force_ssl`.
-
- # ## Configuring the mailer
- #
- # In production you need to configure the mailer to use a different adapter.
- # Also, you may need to configure the Swoosh API client of your choice if you
- # are not using SMTP. Here is an example of the configuration:
- #
- # config :silmataivas, Silmataivas.Mailer,
- # adapter: Swoosh.Adapters.Mailgun,
- # api_key: System.get_env("MAILGUN_API_KEY"),
- # domain: System.get_env("MAILGUN_DOMAIN")
- #
- # For this example you need include a HTTP client required by Swoosh API client.
- # Swoosh supports Hackney and Finch out of the box:
- #
- # config :swoosh, :api_client, Swoosh.ApiClient.Hackney
- #
- # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
-
- config :silmataivas, Silmataivas.Mailer,
- adapter: Swoosh.Adapters.AmazonSES,
- access_key: System.get_env("AWS_ACCESS_KEY_ID"),
- secret: System.get_env("AWS_SECRET_ACCESS_KEY"),
- region: "eu-central-1"
-
- config :silmataivas, Silmataivas.Scheduler,
- jobs: [
- {"0 * * * *", {Silmataivas.WeatherPoller, :check_all, []}}
- ]
-end
diff --git a/config/test.exs b/config/test.exs
deleted file mode 100644
index 7b5e8c3..0000000
--- a/config/test.exs
+++ /dev/null
@@ -1,26 +0,0 @@
-import Config
-
-# Database configuration is now handled in runtime.exs
-# using DB_ADAPTER and DATABASE_URL environment variables
-config :silmataivas, Silmataivas.Repo,
- pool: Ecto.Adapters.SQL.Sandbox,
- pool_size: System.schedulers_online() * 2
-
-# We don't run a server during test. If one is required,
-# you can enable the server option below.
-config :silmataivas, SilmataivasWeb.Endpoint,
- http: [ip: {127, 0, 0, 1}, port: 4002],
- secret_key_base: "QfqSXcc0rT7DLhF/zLnd5MGzXX3+NbSe46do+x4nQs9b4wlNixD0cDHJKsq/faLU",
- server: false
-
-# In test we don't send emails
-config :silmataivas, Silmataivas.Mailer, adapter: Swoosh.Adapters.Test
-
-# Disable swoosh api client as it is only required for production adapters
-config :swoosh, :api_client, false
-
-# Print only warnings and errors during test
-config :logger, level: :warning
-
-# Initialize plugs at runtime for faster test compilation
-config :phoenix, :plug_init_mode, :runtime
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index b360c07..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-services:
- # Silmataivas application
- app:
- build:
- context: .
- dockerfile: Dockerfile
- restart: unless-stopped
- ports:
- - "4000:4000"
- environment:
- - PHX_HOST=localhost
- - SECRET_KEY_BASE=${SECRET_KEY_BASE:-$(openssl rand -base64 48)}
- - DB_ADAPTER=${DB_ADAPTER:-sqlite}
- - DATABASE_URL=${DATABASE_URL:-/app/data/silmataivas.db}
- - OPENWEATHERMAP_API_KEY=${OPENWEATHERMAP_API_KEY}
- - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- volumes:
- - silmataivas_data:/app/data
- # Command to run on container start - will run database migrations before starting the application
- command: sh -c "/app/bin/silmataivas eval 'Silmataivas.Release.setup()' && /app/bin/silmataivas start"
- # networks:
- # - silmataivas_network
- # depends_on:
- # db:
- # condition: service_started
- # required: false
-
- # PostgreSQL database
- # To enable PostgreSQL:
- # 1. Uncomment this section
- # 2. Set DB_ADAPTER=postgres in your environment
- # 3. Set DATABASE_URL to your PostgreSQL connection string
- #db:
- # image: postgres:16-alpine
- # restart: unless-stopped
- # ports:
- # - "5432:5432"
- # environment:
- # - POSTGRES_USER=${PGUSER:-postgres}
- # - POSTGRES_PASSWORD=${PGPASSWORD:-postgres}
- # - POSTGRES_DB=${PGDATABASE:-silmataivas_prod}
- # volumes:
- # - postgres_data:/var/lib/postgresql/data
- # networks:
- # - silmataivas_network
- # # Only start PostgreSQL if DB_ADAPTER is set to postgres
- # profiles:
- # - postgres
-
-volumes:
- silmataivas_data:
- # postgres_data:
-
-# networks:
-# silmataivas_network:
-# driver: bridge
diff --git a/installation/docker-compose.deploy.yml b/installation/docker-compose.deploy.yml
deleted file mode 100644
index 125db4b..0000000
--- a/installation/docker-compose.deploy.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-services:
- # Silmataivas application
- app:
- image: ${DOCKER_IMAGE}
- restart: unless-stopped
- ports:
- - "4000:4000"
- environment:
- - PHX_HOST=${PHX_HOST:-localhost}
- - SECRET_KEY_BASE=${SECRET_KEY_BASE}
- - DB_ADAPTER=${DB_ADAPTER:-sqlite}
- - DATABASE_URL=${DATABASE_URL:-/app/data/silmataivas.db}
- - OPENWEATHERMAP_API_KEY=${OPENWEATHERMAP_API_KEY}
- - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- volumes:
- - silmataivas_data:/app/data
- # Command to run on container start - will run database migrations before starting the application
- command: sh -c "/app/bin/silmataivas eval 'Silmataivas.Release.setup()' && /app/bin/silmataivas start"
- networks:
- - silmataivas_network
- # Uncomment the following lines if using PostgreSQL
- # depends_on:
- # db:
- # condition: service_started
- # required: false
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
- interval: 30s
- timeout: 5s
- retries: 3
- start_period: 10s
-
- # PostgreSQL database
- # To enable PostgreSQL:
- # 1. Uncomment this section
- # 2. Set DB_ADAPTER=postgres in your environment
- # 3. Set DATABASE_URL to your PostgreSQL connection string
- #db:
- # image: postgres:16-alpine
- # restart: unless-stopped
- # ports:
- # - "5432:5432"
- # environment:
- # - POSTGRES_USER=${PGUSER:-postgres}
- # - POSTGRES_PASSWORD=${PGPASSWORD:-postgres}
- # - POSTGRES_DB=${PGDATABASE:-silmataivas_prod}
- # volumes:
- # - postgres_data:/var/lib/postgresql/data
- # networks:
- # - silmataivas_network
- # # Only start PostgreSQL if DB_ADAPTER is set to postgres
- # profiles:
- # - postgres
- # healthcheck:
- # test: ["CMD-SHELL", "pg_isready -U postgres"]
- # interval: 10s
- # timeout: 5s
- # retries: 5
-
-volumes:
- silmataivas_data:
- # postgres_data:
-
-networks:
- silmataivas_network:
- driver: bridge
diff --git a/installation/setup_db.sql b/installation/setup_db.sql
deleted file mode 100644
index 3014dc0..0000000
--- a/installation/setup_db.sql
+++ /dev/null
@@ -1,19 +0,0 @@
--- setup_db.sql
-
--- Create user (if it doesn't exist)
-DO
-$$
-BEGIN
- IF NOT EXISTS (
- SELECT FROM pg_catalog.pg_roles WHERE rolname = 'silmataivas'
- ) THEN
- CREATE ROLE silmataivas LOGIN PASSWORD 'silmataivas';
- END IF;
-END
-$$;
-
--- Create database owned by the user
-CREATE DATABASE silmataivas OWNER silmataivas;
-
--- Optional: grant all privileges explicitly
-GRANT ALL PRIVILEGES ON DATABASE silmataivas TO silmataivas; \ No newline at end of file
diff --git a/installation/silmataivas.nginx b/installation/silmataivas.nginx
deleted file mode 100644
index 5b58a89..0000000
--- a/installation/silmataivas.nginx
+++ /dev/null
@@ -1,81 +0,0 @@
-# default nginx site config for Silmataivas
-#
-# Simple installation instructions:
-# 1. Install your TLS certificate, possibly using Let's Encrypt.
-# 2. Replace 'example.tld' with your instance's domain wherever it appears.
-# 3. Copy this file to /etc/nginx/sites-available/ and then add a symlink to it
-# in /etc/nginx/sites-enabled/ and run 'nginx -s reload' or restart nginx.
-
-# this is explicitly IPv4 since Silmataivas.Web.Endpoint binds on IPv4 only
-# and `localhost.` resolves to [::0] on some systems: see issue #930
-upstream phoenix {
- server 127.0.0.1:4000 max_fails=5 fail_timeout=60s;
-}
-
-server {
- server_name example.tld;
-
- listen 80;
- listen [::]:80;
-
- # Uncomment this if you need to use the 'webroot' method with certbot. Make sure
- # that the directory exists and that it is accessible by the webserver. If you followed
- # the guide, you already ran 'mkdir -p /var/lib/letsencrypt' to create the folder.
- # You may need to load this file with the ssl server block commented out, run certbot
- # to get the certificate, and then uncomment it.
- #
- # location ~ /\.well-known/acme-challenge {
- # root /var/lib/letsencrypt/;
- # }
- location / {
- return 301 https://$server_name$request_uri;
- }
-}
-
-# Enable SSL session caching for improved performance
-ssl_session_cache shared:ssl_session_cache:10m;
-
-server {
- server_name example.tld;
-
- listen 443 ssl;
- listen [::]:443 ssl;
- http2 on;
- ssl_session_timeout 1d;
- ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
- ssl_session_tickets off;
-
- ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
- ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
-
- ssl_protocols TLSv1.2 TLSv1.3;
- ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
- ssl_prefer_server_ciphers off;
- # In case of an old server with an OpenSSL version of 1.0.2 or below,
- # leave only prime256v1 or comment out the following line.
- ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
- ssl_stapling on;
- ssl_stapling_verify on;
-
- gzip_vary on;
- gzip_proxied any;
- gzip_comp_level 6;
- gzip_buffers 16 8k;
- gzip_http_version 1.1;
- gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml;
-
- # the nginx default is 1m, not enough for large media uploads
- client_max_body_size 16m;
- ignore_invalid_headers off;
-
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_set_header Host $http_host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-
- location / {
- proxy_pass http://phoenix;
- }
-}
diff --git a/installation/silmataivas.service b/installation/silmataivas.service
deleted file mode 100644
index 811415d..0000000
--- a/installation/silmataivas.service
+++ /dev/null
@@ -1,48 +0,0 @@
-[Unit]
-Description=Silmataivas weather monitoring
-After=network.target postgresql.service
-
-[Service]
-KillMode=process
-Restart=on-failure
-RestartSec=5
-KillSignal=SIGTERM
-
-; Name of the user that runs the silmataivas service.
-User=silmataivas
-; Declares that silmataivas runs in production mode.
-Environment="MIX_ENV=prod"
-Environment=LANG=en_US.UTF-8
-Environment=LC_ALL=en_US.UTF-8
-
-;Read secrets for config
-EnvironmentFile=/etc/silmataivas/env
-
-; Make sure that all paths fit your installation.
-; Path to the home directory of the user running the Silmataivas service.
-Environment="HOME=/var/lib/silmataivas"
-; Path to the folder containing the Silmataivas installation.
-WorkingDirectory=/opt/silmataivas
-; Path to the Mix binary.
-ExecStart=/opt/silmataivas/bin/silmataivas start
-ExecStop=/opt/silmataivas/bin/silmataivas stop
-ExecReload=/opt/silmataivas/bin/silmataivas restart
-ExecStartPre=/opt/silmataivas/bin/silmataivas eval 'Silmataivas.Release.migrate()'
-
-; Some security directives.
-; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops.
-PrivateTmp=true
-; The /home, /root, and /run/user folders can not be accessed by this service anymore. If your Silmataivas user has its home folder in one of the restricted places, or use one of these folders as its working directory, you have to set this to false.
-ProtectHome=true
-; Mount /usr, /boot, and /etc as read-only for processes invoked by this service.
-ProtectSystem=full
-; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled by default because it may not work on devices like the Raspberry Pi.
-PrivateDevices=false
-; Drops the sysadmin capability from the daemon.
-CapabilityBoundingSet=~CAP_SYS_ADMIN
-
-StandardOutput=journal
-StandardError=journal
-
-[Install]
-WantedBy=multi-user.target
diff --git a/lefthook.yml b/lefthook.yml
index 12b495a..b855ccb 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -10,6 +10,3 @@ pre-commit:
yamllint:
glob: "**/*.{yml,yaml}"
run: yamllint {staged_files}
- elixir:
- glob: "**/*.{ex,exs}"
- run: mix format --check-formatted {staged_files} \ No newline at end of file
diff --git a/lib/mix/tasks/silmataivas.user.new.ex b/lib/mix/tasks/silmataivas.user.new.ex
deleted file mode 100644
index fe10c7f..0000000
--- a/lib/mix/tasks/silmataivas.user.new.ex
+++ /dev/null
@@ -1,48 +0,0 @@
-defmodule Mix.Tasks.Silmataivas.User.New do
- use Mix.Task
-
- @shortdoc "Creates a new user and prints its API token."
-
- @moduledoc """
- Creates a new user.
-
- mix silmataivas.user.new
- mix silmataivas.user.new <user_id>
- mix silmataivas.user.new <user_id> <role>
-
- This task starts the application and creates a user using the Silmataivas.Users context.
-
- ## Options
- * `<user_id>` - An optional user ID to use. If not provided, a UUID will be generated.
- * `<role>` - An optional role, must be either "user" or "admin". Defaults to "user".
- """
-
- def run(args) do
- Mix.Task.run("app.start", [])
-
- {user_id, role} =
- case args do
- [provided_id, provided_role | _] -> {provided_id, provided_role}
- [provided_id | _] -> {provided_id, "user"}
- [] -> {Ecto.UUID.generate(), "user"}
- end
-
- # Validate role
- unless role in ["user", "admin"] do
- Mix.raise("Invalid role: #{role}. Role must be either \"user\" or \"admin\".")
- end
-
- user_params = %{user_id: user_id, role: role}
-
- case Silmataivas.Users.create_user(user_params) do
- {:ok, user} ->
- IO.puts("\n✅ User created successfully!")
- IO.puts(" User ID (API token): #{user.user_id}")
- IO.puts(" Role: #{user.role}")
-
- {:error, changeset} ->
- IO.puts("\n❌ Failed to create user:")
- IO.inspect(changeset.errors)
- end
- end
-end
diff --git a/lib/silmataivas.ex b/lib/silmataivas.ex
deleted file mode 100644
index ba6dfcf..0000000
--- a/lib/silmataivas.ex
+++ /dev/null
@@ -1,9 +0,0 @@
-defmodule Silmataivas do
- @moduledoc """
- Silmataivas keeps the contexts that define your domain
- and business logic.
-
- Contexts are also responsible for managing your data, regardless
- if it comes from the database, an external API or others.
- """
-end
diff --git a/lib/silmataivas/application.ex b/lib/silmataivas/application.ex
deleted file mode 100644
index 269f48f..0000000
--- a/lib/silmataivas/application.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-defmodule Silmataivas.Application do
- # See https://hexdocs.pm/elixir/Application.html
- # for more information on OTP Applications
- @moduledoc false
-
- use Application
-
- @impl true
- def start(_type, _args) do
- children = [
- SilmataivasWeb.Telemetry,
- Silmataivas.Repo,
- {DNSCluster, query: Application.get_env(:silmataivas, :dns_cluster_query, :ignore)},
- {Phoenix.PubSub, name: Silmataivas.PubSub},
- # Start the Finch HTTP client for sending emails
- {Finch, name: Silmataivas.Finch},
- # Start a worker by calling: Silmataivas.Worker.start_link(arg)
- # {Silmataivas.Worker, arg},
- # Start to serve requests, typically the last entry
- SilmataivasWeb.Endpoint,
- Silmataivas.Scheduler
- ]
-
- # See https://hexdocs.pm/elixir/Supervisor.html
- # for other strategies and supported options
- opts = [strategy: :one_for_one, name: Silmataivas.Supervisor]
- Supervisor.start_link(children, opts)
- end
-
- # Tell Phoenix to update the endpoint configuration
- # whenever the application is updated.
- @impl true
- def config_change(changed, _new, removed) do
- SilmataivasWeb.Endpoint.config_change(changed, removed)
- :ok
- end
-end
diff --git a/lib/silmataivas/locations.ex b/lib/silmataivas/locations.ex
deleted file mode 100644
index 2fc33dc..0000000
--- a/lib/silmataivas/locations.ex
+++ /dev/null
@@ -1,104 +0,0 @@
-defmodule Silmataivas.Locations do
- @moduledoc """
- The Locations context.
- """
-
- import Ecto.Query, warn: false
- alias Silmataivas.Repo
-
- alias Silmataivas.Locations.Location
-
- @doc """
- Returns the list of locations.
-
- ## Examples
-
- iex> list_locations()
- [%Location{}, ...]
-
- """
- def list_locations do
- Repo.all(Location)
- end
-
- @doc """
- Gets a single location.
-
- Raises `Ecto.NoResultsError` if the Location does not exist.
-
- ## Examples
-
- iex> get_location!(123)
- %Location{}
-
- iex> get_location!(456)
- ** (Ecto.NoResultsError)
-
- """
- def get_location!(id), do: Repo.get!(Location, id)
-
- @doc """
- Creates a location.
-
- ## Examples
-
- iex> create_location(%{field: value})
- {:ok, %Location{}}
-
- iex> create_location(%{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def create_location(attrs \\ %{}) do
- %Location{}
- |> Location.changeset(attrs)
- |> Repo.insert()
- end
-
- @doc """
- Updates a location.
-
- ## Examples
-
- iex> update_location(location, %{field: new_value})
- {:ok, %Location{}}
-
- iex> update_location(location, %{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def update_location(%Location{} = location, attrs) do
- location
- |> Location.changeset(attrs)
- |> Repo.update()
- end
-
- @doc """
- Deletes a location.
-
- ## Examples
-
- iex> delete_location(location)
- {:ok, %Location{}}
-
- iex> delete_location(location)
- {:error, %Ecto.Changeset{}}
-
- """
- def delete_location(%Location{} = location) do
- Repo.delete(location)
- end
-
- @doc """
- Returns an `%Ecto.Changeset{}` for tracking location changes.
-
- ## Examples
-
- iex> change_location(location)
- %Ecto.Changeset{data: %Location{}}
-
- """
- def change_location(%Location{} = location, attrs \\ %{}) do
- Location.changeset(location, attrs)
- end
-end
diff --git a/lib/silmataivas/locations/location.ex b/lib/silmataivas/locations/location.ex
deleted file mode 100644
index 7da7290..0000000
--- a/lib/silmataivas/locations/location.ex
+++ /dev/null
@@ -1,19 +0,0 @@
-defmodule Silmataivas.Locations.Location do
- use Ecto.Schema
- import Ecto.Changeset
-
- schema "locations" do
- field :latitude, :float
- field :longitude, :float
- field :user_id, :id
-
- timestamps(type: :utc_datetime)
- end
-
- @doc false
- def changeset(location, attrs) do
- location
- |> cast(attrs, [:latitude, :longitude, :user_id])
- |> validate_required([:latitude, :longitude, :user_id])
- end
-end
diff --git a/lib/silmataivas/mailer.ex b/lib/silmataivas/mailer.ex
deleted file mode 100644
index 3c11436..0000000
--- a/lib/silmataivas/mailer.ex
+++ /dev/null
@@ -1,44 +0,0 @@
-defmodule Silmataivas.Mailer do
- use Swoosh.Mailer, otp_app: :silmataivas
- require Logger
-
- def send_alert(
- email,
- %{
- "main" => %{"temp" => temp},
- "wind" => %{"speed" => speed},
- "dt_txt" => time_str
- } = entry
- ) do
- rain_mm = get_in(entry, ["rain", "3h"]) || 0.0
- wind_kmh = speed * 3.6
-
- import Swoosh.Email
-
- body = """
- 🚨 Weather alert for your location (#{time_str}):
-
- 🌬️ Wind: #{Float.round(wind_kmh, 1)} km/h
- 🌧️ Rain: #{rain_mm} mm
- 🌡️ Temperature: #{temp} °C
-
- Stay safe,
- — Silmätaivas
- """
-
- email_struct =
- new()
- |> to(email)
- |> from({"Silmätaivas Alerts", "silmataivas@rycerz.cloud"})
- |> subject("⚠️ Weather Alert for Your Location")
- |> text_body(body)
-
- case deliver(email_struct) do
- {:ok, response} ->
- Logger.info("📨 Email sent via SES: #{inspect(response)}")
-
- {:error, reason} ->
- Logger.error("❌ Failed to send email: #{inspect(reason)}")
- end
- end
-end
diff --git a/lib/silmataivas/ntfy_notifier.ex b/lib/silmataivas/ntfy_notifier.ex
deleted file mode 100644
index 26815db..0000000
--- a/lib/silmataivas/ntfy_notifier.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-defmodule Silmataivas.Notifications.NtfyNotifier do
- @moduledoc """
- Sends push notifications using ntfy.sh.
- """
-
- @ntfy_url System.get_env("NTFY_URL") || "https://ntfy.sh"
-
- def send_alert(
- topic,
- %{
- "main" => %{"temp" => temp},
- "wind" => %{"speed" => speed},
- "dt_txt" => time_str
- } = entry
- ) do
- rain_mm = get_in(entry, ["rain", "3h"]) || 0.0
- wind_kmh = speed * 3.6
-
- message = """
- 🚨 Weather alert for your location (#{time_str}):
-
- 🌬️ Wind: #{Float.round(wind_kmh, 1)} km/h
- 🌧️ Rain: #{rain_mm} mm
- 🌡️ Temperature: #{temp} °C
-
- Stay safe,
- — Silmätaivas
- """
-
- Req.post("#{@ntfy_url}/#{topic}",
- headers: [{"Priority", "5"}],
- body: message
- )
- end
-end
diff --git a/lib/silmataivas/release.ex b/lib/silmataivas/release.ex
deleted file mode 100644
index 4fc9e93..0000000
--- a/lib/silmataivas/release.ex
+++ /dev/null
@@ -1,136 +0,0 @@
-defmodule Silmataivas.Release do
- @moduledoc """
- Release tasks for Silmataivas application.
-
- This module provides functions to run Ecto migrations in a
- compiled release, supporting both SQLite and PostgreSQL backends.
- """
-
- @app :silmataivas
-
- @doc """
- Creates a new user with optional user ID and role.
-
- ## Parameters
- * `user_id` - An optional user ID to use. If not provided, a UUID will be generated.
- * `role` - An optional role, must be either "user" or "admin". Defaults to "user".
-
- ## Examples
- Silmataivas.Release.new_user()
- Silmataivas.Release.new_user("custom_user_id")
- Silmataivas.Release.new_user("custom_user_id", "admin")
- """
- def new_user(user_id \\ nil, role \\ "user") do
- # Create the new user
- load_app()
- start_repos()
-
- # Validate role
- unless role in ["user", "admin"] do
- IO.puts("\n❌ Invalid role: #{role}. Role must be either \"user\" or \"admin\".")
- exit({:shutdown, 1})
- end
-
- user_id = user_id || Ecto.UUID.generate()
- user_params = %{user_id: user_id, role: role}
-
- case Silmataivas.Users.create_user(user_params) do
- {:ok, user} ->
- IO.puts("\n✅ User created successfully!")
- IO.puts(" User ID (API token): #{user.user_id}")
- IO.puts(" Role: #{user.role}")
-
- {:error, changeset} ->
- IO.puts("\n❌ Failed to create user:")
- IO.inspect(changeset.errors)
- end
- end
-
- def migrate do
- load_app()
-
- for repo <- repos() do
- {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
- end
- end
-
- def rollback(repo, version) do
- load_app()
-
- {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
- end
-
- def create_db do
- load_app()
-
- for repo <- repos() do
- # Create the database if it doesn't exist
- adapter = get_repo_adapter(repo)
-
- case adapter.storage_up(repo.config()) do
- :ok ->
- IO.puts("Database for #{inspect(repo)} created successfully")
-
- {:error, :already_up} ->
- IO.puts("Database for #{inspect(repo)} already exists")
-
- {:error, reason} ->
- IO.warn("Database for #{inspect(repo)} failed to create: #{inspect(reason)}")
- end
- end
- end
-
- def setup do
- # Create the database and then run migrations
- create_db()
- migrate()
- end
-
- def db_info do
- load_app()
-
- for repo <- repos() do
- adapter = get_repo_adapter(repo)
- config = repo.config()
-
- IO.puts("Repository: #{inspect(repo)}")
- IO.puts("Adapter: #{inspect(adapter)}")
-
- case adapter do
- Ecto.Adapters.SQLite3 ->
- db_path = config[:database] || "default.db"
- IO.puts("Database path: #{db_path}")
-
- Ecto.Adapters.Postgres ->
- hostname = config[:hostname] || "localhost"
- database = config[:database] || "default"
- IO.puts("Host: #{hostname}, Database: #{database}")
-
- _ ->
- IO.puts("Config: #{inspect(config)}")
- end
-
- IO.puts("---")
- end
- end
-
- defp get_repo_adapter(repo) do
- repo.config()[:adapter]
- end
-
- defp start_repos do
- {:ok, _} = Application.ensure_all_started(:ecto_sql)
-
- for repo <- repos() do
- {:ok, _} = repo.start_link(pool_size: 2)
- end
- end
-
- defp repos do
- Application.fetch_env!(@app, :ecto_repos)
- end
-
- defp load_app do
- Application.load(@app)
- end
-end
diff --git a/lib/silmataivas/repo.ex b/lib/silmataivas/repo.ex
deleted file mode 100644
index d1bbcca..0000000
--- a/lib/silmataivas/repo.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule Silmataivas.Repo do
- use Ecto.Repo,
- otp_app: :silmataivas,
- adapter: Ecto.Adapters.SQLite3
-
- @doc """
- Dynamic adapter configuration based on application environment.
-
- This will be automatically called by Ecto during startup.
- """
- def init(_type, config) do
- # Check for adapter in config, fall back to Ecto.Adapters.SQLite3
- adapter =
- config[:adapter] ||
- Application.get_env(:silmataivas, Silmataivas.Repo, [])[:adapter] ||
- Ecto.Adapters.SQLite3
-
- {:ok, Keyword.put(config, :adapter, adapter)}
- end
-end
diff --git a/lib/silmataivas/scheduler.ex b/lib/silmataivas/scheduler.ex
deleted file mode 100644
index 3e04f7e..0000000
--- a/lib/silmataivas/scheduler.ex
+++ /dev/null
@@ -1,4 +0,0 @@
-# lib/silmataivas/scheduler.ex
-defmodule Silmataivas.Scheduler do
- use Quantum, otp_app: :silmataivas
-end
diff --git a/lib/silmataivas/users.ex b/lib/silmataivas/users.ex
deleted file mode 100644
index 1fcefd4..0000000
--- a/lib/silmataivas/users.ex
+++ /dev/null
@@ -1,124 +0,0 @@
-defmodule Silmataivas.Users do
- @moduledoc """
- The Users context.
- """
-
- import Ecto.Query, warn: false
- alias Silmataivas.Repo
-
- alias Silmataivas.Users.User
-
- @doc """
- Returns the list of users.
-
- ## Examples
-
- iex> list_users()
- [%User{}, ...]
-
- """
- def list_users do
- Repo.all(User)
- end
-
- @doc """
- Gets a single user.
-
- Raises `Ecto.NoResultsError` if the User does not exist.
-
- ## Examples
-
- iex> get_user!(123)
- %User{}
-
- iex> get_user!(456)
- ** (Ecto.NoResultsError)
-
- """
- def get_user!(id), do: Repo.get!(User, id)
-
- @doc """
- Gets a user by user_id.
-
- ## Examples
-
- iex> get_user_by_user_id("some_user_id")
- %User{}
-
- iex> get_user_by_user_id("non_existent_user_id")
- nil
-
- """
- def get_user_by_user_id(user_id) do
- Repo.get_by(User, user_id: user_id)
- end
-
- @doc """
- Creates a user.
-
- ## Examples
-
- iex> create_user(%{field: value})
- {:ok, %User{}}
-
- iex> create_user(%{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def create_user(attrs \\ %{}) do
- %User{}
- |> User.changeset(attrs)
- |> Repo.insert()
- end
-
- @doc """
- Updates a user.
-
- ## Examples
-
- iex> update_user(user, %{field: new_value})
- {:ok, %User{}}
-
- iex> update_user(user, %{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def update_user(%User{} = user, attrs) do
- user
- |> User.changeset(attrs)
- |> Repo.update()
- end
-
- @doc """
- Deletes a user.
-
- ## Examples
-
- iex> delete_user(user)
- {:ok, %User{}}
-
- iex> delete_user(user)
- {:error, %Ecto.Changeset{}}
-
- """
- def delete_user(%User{} = user) do
- Repo.delete(user)
- end
-
- @doc """
- Returns an `%Ecto.Changeset{}` for tracking user changes.
-
- ## Examples
-
- iex> change_user(user)
- %Ecto.Changeset{data: %User{}}
-
- """
- def change_user(%User{} = user, attrs \\ %{}) do
- User.changeset(user, attrs)
- end
-
- def list_users_with_locations do
- Repo.all(from u in User, preload: [:location])
- end
-end
diff --git a/lib/silmataivas/users/user.ex b/lib/silmataivas/users/user.ex
deleted file mode 100644
index b0746cd..0000000
--- a/lib/silmataivas/users/user.ex
+++ /dev/null
@@ -1,29 +0,0 @@
-defmodule Silmataivas.Users.User do
- use Ecto.Schema
- import Ecto.Changeset
- alias Silmataivas.Repo
-
- @roles ["user", "admin"]
-
- schema "users" do
- field :user_id, :string
- field :role, :string, default: "user"
- has_one :location, Silmataivas.Locations.Location
- timestamps(type: :utc_datetime)
- end
-
- @doc false
- def changeset(user, attrs) do
- user
- |> cast(attrs, [:user_id, :role])
- |> validate_required([:user_id])
- |> validate_inclusion(:role, @roles)
- |> unique_constraint(:user_id)
- end
-
- def create_user(attrs \\ %{}) do
- %__MODULE__{}
- |> changeset(attrs)
- |> Repo.insert()
- end
-end
diff --git a/lib/silmataivas/weather_poller.ex b/lib/silmataivas/weather_poller.ex
deleted file mode 100644
index b42b184..0000000
--- a/lib/silmataivas/weather_poller.ex
+++ /dev/null
@@ -1,102 +0,0 @@
-defmodule Silmataivas.WeatherPoller do
- require Logger
- alias Silmataivas.{Users, Notifications.NtfyNotifier}
-
- @api_url "https://api.openweathermap.org/data/2.5/forecast"
- # Check forecasts within the next 24 hours
- @alert_window_hours 24
-
- def check_all do
- Logger.info("🔄 Checking weather forecast for all users...")
-
- Users.list_users_with_locations()
- |> Enum.each(&check_user_weather/1)
- end
-
- def check_user_weather(%{user_id: user_id, location: %{latitude: lat, longitude: lon}} = _user) do
- case fetch_forecast(lat, lon) do
- {:ok, forecasts} ->
- case find_first_alert_entry(forecasts) do
- nil -> :ok
- entry -> NtfyNotifier.send_alert(user_id, entry)
- end
-
- {:error, reason} ->
- Logger.error("❌ Error fetching forecast for user #{user_id}: #{inspect(reason)}")
- end
- end
-
- # Add this clause to handle users with missing location data
- def check_user_weather(%{user_id: user_id} = user) do
- Logger.warning(
- "⚠️ User #{user_id} has missing or incomplete location data: #{inspect(user)}",
- []
- )
-
- :ok
- end
-
- # Add a catch-all clause to handle unexpected data formats
- def check_user_weather(invalid_user) do
- Logger.error("❌ Invalid user data structure: #{inspect(invalid_user)}")
- :ok
- end
-
- defp fetch_forecast(lat, lon) do
- api_key = Application.fetch_env!(:silmataivas, :openweathermap_api_key)
-
- Req.get(
- url: @api_url,
- params: [
- lat: lat,
- lon: lon,
- units: "metric",
- appid: api_key
- ]
- )
- |> case do
- {:ok, %{status: 200, body: %{"list" => forecast_list}}} ->
- {:ok, forecast_list}
-
- {:ok, %{status: code, body: body}} ->
- {:error, {code, body}}
-
- error ->
- error
- end
- end
-
- defp dangerous_conditions?(
- %{
- "main" => %{"temp" => temp},
- "wind" => %{"speed" => speed},
- "dt_txt" => time_str
- } = entry
- ) do
- rain_mm = get_in(entry, ["rain", "3h"]) || 0.0
- wind_kmh = speed * 3.6
-
- cond do
- wind_kmh > 80 -> log_reason("Wind", wind_kmh, time_str)
- rain_mm > 40 -> log_reason("Rain", rain_mm, time_str)
- temp < 0 -> log_reason("Temperature", temp, time_str)
- true -> false
- end
- end
-
- defp find_first_alert_entry(forecast_list) do
- now = DateTime.utc_now()
-
- forecast_list
- |> Enum.take_while(fn %{"dt" => ts} ->
- forecast_time = DateTime.from_unix!(ts)
- DateTime.diff(forecast_time, now, :hour) <= @alert_window_hours
- end)
- |> Enum.find(&dangerous_conditions?/1)
- end
-
- defp log_reason(type, value, time_str) do
- Logger.info("🚨 #{type} threshold exceeded: #{value} at #{time_str}")
- true
- end
-end
diff --git a/lib/silmataivas_web.ex b/lib/silmataivas_web.ex
deleted file mode 100644
index ef60499..0000000
--- a/lib/silmataivas_web.ex
+++ /dev/null
@@ -1,67 +0,0 @@
-defmodule SilmataivasWeb do
- @moduledoc """
- The entrypoint for defining your web interface, such
- as controllers, components, channels, and so on.
-
- This can be used in your application as:
-
- use SilmataivasWeb, :controller
- use SilmataivasWeb, :html
-
- The definitions below will be executed for every controller,
- component, etc, so keep them short and clean, focused
- on imports, uses and aliases.
-
- Do NOT define functions inside the quoted expressions
- below. Instead, define additional modules and import
- those modules here.
- """
-
- def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
-
- def router do
- quote do
- use Phoenix.Router, helpers: false
-
- # Import common connection and controller functions to use in pipelines
- import Plug.Conn
- import Phoenix.Controller
- end
- end
-
- def channel do
- quote do
- use Phoenix.Channel
- end
- end
-
- def controller do
- quote do
- use Phoenix.Controller,
- formats: [:html, :json],
- layouts: [html: SilmataivasWeb.Layouts]
-
- use Gettext, backend: SilmataivasWeb.Gettext
-
- import Plug.Conn
-
- unquote(verified_routes())
- end
- end
-
- def verified_routes do
- quote do
- use Phoenix.VerifiedRoutes,
- endpoint: SilmataivasWeb.Endpoint,
- router: SilmataivasWeb.Router,
- statics: SilmataivasWeb.static_paths()
- end
- end
-
- @doc """
- When used, dispatch to the appropriate controller/live_view/etc.
- """
- defmacro __using__(which) when is_atom(which) do
- apply(__MODULE__, which, [])
- end
-end
diff --git a/lib/silmataivas_web/controllers/changeset_json.ex b/lib/silmataivas_web/controllers/changeset_json.ex
deleted file mode 100644
index ac0226d..0000000
--- a/lib/silmataivas_web/controllers/changeset_json.ex
+++ /dev/null
@@ -1,25 +0,0 @@
-defmodule SilmataivasWeb.ChangesetJSON do
- @doc """
- Renders changeset errors.
- """
- def error(%{changeset: changeset}) do
- # When encoded, the changeset returns its errors
- # as a JSON object. So we just pass it forward.
- %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
- end
-
- defp translate_error({msg, opts}) do
- # You can make use of gettext to translate error messages by
- # uncommenting and adjusting the following code:
-
- # if count = opts[:count] do
- # Gettext.dngettext(SilmataivasWeb.Gettext, "errors", msg, msg, count, opts)
- # else
- # Gettext.dgettext(SilmataivasWeb.Gettext, "errors", msg, opts)
- # end
-
- Enum.reduce(opts, msg, fn {key, value}, acc ->
- String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
- end)
- end
-end
diff --git a/lib/silmataivas_web/controllers/error_json.ex b/lib/silmataivas_web/controllers/error_json.ex
deleted file mode 100644
index a2ca902..0000000
--- a/lib/silmataivas_web/controllers/error_json.ex
+++ /dev/null
@@ -1,21 +0,0 @@
-defmodule SilmataivasWeb.ErrorJSON do
- @moduledoc """
- This module is invoked by your endpoint in case of errors on JSON requests.
-
- See config/config.exs.
- """
-
- # If you want to customize a particular status code,
- # you may add your own clauses, such as:
- #
- # def render("500.json", _assigns) do
- # %{errors: %{detail: "Internal Server Error"}}
- # end
-
- # By default, Phoenix returns the status message from
- # the template name. For example, "404.json" becomes
- # "Not Found".
- def render(template, _assigns) do
- %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
- end
-end
diff --git a/lib/silmataivas_web/controllers/fallback_controller.ex b/lib/silmataivas_web/controllers/fallback_controller.ex
deleted file mode 100644
index f315110..0000000
--- a/lib/silmataivas_web/controllers/fallback_controller.ex
+++ /dev/null
@@ -1,24 +0,0 @@
-defmodule SilmataivasWeb.FallbackController do
- @moduledoc """
- Translates controller action results into valid `Plug.Conn` responses.
-
- See `Phoenix.Controller.action_fallback/1` for more details.
- """
- use SilmataivasWeb, :controller
-
- # This clause handles errors returned by Ecto's insert/update/delete.
- def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
- conn
- |> put_status(:unprocessable_entity)
- |> put_view(json: SilmataivasWeb.ChangesetJSON)
- |> render(:error, changeset: changeset)
- end
-
- # This clause is an example of how to handle resources that cannot be found.
- def call(conn, {:error, :not_found}) do
- conn
- |> put_status(:not_found)
- |> put_view(html: SilmataivasWeb.ErrorHTML, json: SilmataivasWeb.ErrorJSON)
- |> render(:"404")
- end
-end
diff --git a/lib/silmataivas_web/controllers/health_controller.ex b/lib/silmataivas_web/controllers/health_controller.ex
deleted file mode 100644
index 959b84b..0000000
--- a/lib/silmataivas_web/controllers/health_controller.ex
+++ /dev/null
@@ -1,9 +0,0 @@
-defmodule SilmataivasWeb.HealthController do
- use SilmataivasWeb, :controller
-
- def index(conn, _params) do
- conn
- |> put_status(:ok)
- |> json(%{status: "ok"})
- end
-end
diff --git a/lib/silmataivas_web/controllers/location_controller.ex b/lib/silmataivas_web/controllers/location_controller.ex
deleted file mode 100644
index d494d59..0000000
--- a/lib/silmataivas_web/controllers/location_controller.ex
+++ /dev/null
@@ -1,46 +0,0 @@
-defmodule SilmataivasWeb.LocationController do
- use SilmataivasWeb, :controller
-
- alias Silmataivas.Locations
- alias Silmataivas.Locations.Location
-
- action_fallback SilmataivasWeb.FallbackController
-
- def index(conn, _params) do
- locations = Locations.list_locations()
- render(conn, :index, locations: locations)
- end
-
- def create(conn, params) do
- user = conn.assigns.current_user
- params = Map.put(params, "user_id", user.id)
-
- with {:ok, %Location{} = location} <- Locations.create_location(params) do
- conn
- |> put_status(:created)
- |> put_resp_header("location", ~p"/api/locations/#{location}")
- |> render(:show, location: location)
- end
- end
-
- def show(conn, %{"id" => id}) do
- location = Locations.get_location!(id)
- render(conn, :show, location: location)
- end
-
- def update(conn, %{"id" => id, "location" => location_params}) do
- location = Locations.get_location!(id)
-
- with {:ok, %Location{} = location} <- Locations.update_location(location, location_params) do
- render(conn, :show, location: location)
- end
- end
-
- def delete(conn, %{"id" => id}) do
- location = Locations.get_location!(id)
-
- with {:ok, %Location{}} <- Locations.delete_location(location) do
- send_resp(conn, :no_content, "")
- end
- end
-end
diff --git a/lib/silmataivas_web/controllers/location_json.ex b/lib/silmataivas_web/controllers/location_json.ex
deleted file mode 100644
index db7e469..0000000
--- a/lib/silmataivas_web/controllers/location_json.ex
+++ /dev/null
@@ -1,25 +0,0 @@
-defmodule SilmataivasWeb.LocationJSON do
- alias Silmataivas.Locations.Location
-
- @doc """
- Renders a list of locations.
- """
- def index(%{locations: locations}) do
- %{data: for(location <- locations, do: data(location))}
- end
-
- @doc """
- Renders a single location.
- """
- def show(%{location: location}) do
- %{data: data(location)}
- end
-
- defp data(%Location{} = location) do
- %{
- id: location.id,
- latitude: location.latitude,
- longitude: location.longitude
- }
- end
-end
diff --git a/lib/silmataivas_web/endpoint.ex b/lib/silmataivas_web/endpoint.ex
deleted file mode 100644
index 086b1f9..0000000
--- a/lib/silmataivas_web/endpoint.ex
+++ /dev/null
@@ -1,51 +0,0 @@
-defmodule SilmataivasWeb.Endpoint do
- use Phoenix.Endpoint, otp_app: :silmataivas
-
- # The session will be stored in the cookie and signed,
- # this means its contents can be read but not tampered with.
- # Set :encryption_salt if you would also like to encrypt it.
- @session_options [
- store: :cookie,
- key: "_silmataivas_key",
- signing_salt: "Fvhz8Cqb",
- same_site: "Lax"
- ]
-
- socket "/live", Phoenix.LiveView.Socket,
- websocket: [connect_info: [session: @session_options]],
- longpoll: [connect_info: [session: @session_options]]
-
- # Serve at "/" the static files from "priv/static" directory.
- #
- # You should set gzip to true if you are running phx.digest
- # when deploying your static files in production.
- plug Plug.Static,
- at: "/",
- from: :silmataivas,
- gzip: false,
- only: SilmataivasWeb.static_paths()
-
- # Code reloading can be explicitly enabled under the
- # :code_reloader configuration of your endpoint.
- if code_reloading? do
- plug Phoenix.CodeReloader
- plug Phoenix.Ecto.CheckRepoStatus, otp_app: :silmataivas
- end
-
- plug Phoenix.LiveDashboard.RequestLogger,
- param_key: "request_logger",
- cookie_key: "request_logger"
-
- plug Plug.RequestId
- plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
-
- plug Plug.Parsers,
- parsers: [:urlencoded, :multipart, :json],
- pass: ["*/*"],
- json_decoder: Phoenix.json_library()
-
- plug Plug.MethodOverride
- plug Plug.Head
- plug Plug.Session, @session_options
- plug SilmataivasWeb.Router
-end
diff --git a/lib/silmataivas_web/gettext.ex b/lib/silmataivas_web/gettext.ex
deleted file mode 100644
index a494c80..0000000
--- a/lib/silmataivas_web/gettext.ex
+++ /dev/null
@@ -1,25 +0,0 @@
-defmodule SilmataivasWeb.Gettext do
- @moduledoc """
- A module providing Internationalization with a gettext-based API.
-
- By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations
- that you can use in your application. To use this Gettext backend module,
- call `use Gettext` and pass it as an option:
-
- use Gettext, backend: SilmataivasWeb.Gettext
-
- # Simple translation
- gettext("Here is the string to translate")
-
- # Plural translation
- ngettext("Here is the string to translate",
- "Here are the strings to translate",
- 3)
-
- # Domain-based translation
- dgettext("errors", "Here is the error message to translate")
-
- See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
- """
- use Gettext.Backend, otp_app: :silmataivas
-end
diff --git a/lib/silmataivas_web/plugs/admin_only.ex b/lib/silmataivas_web/plugs/admin_only.ex
deleted file mode 100644
index b3f21dc..0000000
--- a/lib/silmataivas_web/plugs/admin_only.ex
+++ /dev/null
@@ -1,8 +0,0 @@
-defmodule SilmataivasWeb.Plugs.AdminOnly do
- import Plug.Conn
-
- def init(opts), do: opts
-
- def call(%{assigns: %{current_user: %{role: "admin"}}} = conn, _opts), do: conn
- def call(conn, _opts), do: send_resp(conn, 403, "Forbidden") |> halt()
-end
diff --git a/lib/silmataivas_web/plugs/auth.ex b/lib/silmataivas_web/plugs/auth.ex
deleted file mode 100644
index ff5d25b..0000000
--- a/lib/silmataivas_web/plugs/auth.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule SilmataivasWeb.Plugs.Auth do
- import Plug.Conn
- alias Silmataivas.Users
- alias Silmataivas.Repo
-
- def init(opts), do: opts
-
- def call(conn, _opts) do
- with ["Bearer " <> user_id] <- get_req_header(conn, "authorization"),
- %Users.User{} = user <- Users.get_user_by_user_id(user_id),
- loaded_user <- Repo.preload(user, :location) do
- assign(conn, :current_user, loaded_user)
- else
- _ ->
- conn
- |> send_resp(:unauthorized, "Unauthorized")
- |> halt()
- end
- end
-end
diff --git a/lib/silmataivas_web/router.ex b/lib/silmataivas_web/router.ex
deleted file mode 100644
index d790ef9..0000000
--- a/lib/silmataivas_web/router.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-defmodule SilmataivasWeb.Router do
- use SilmataivasWeb, :router
-
- pipeline :api do
- plug :accepts, ["json"]
- plug SilmataivasWeb.Plugs.Auth
- end
-
- pipeline :api_public do
- plug :accepts, ["json"]
- end
-
- scope "/api", SilmataivasWeb do
- pipe_through :api
-
- resources "/locations", LocationController, only: [:index, :create, :show, :update]
- end
-
- scope "/", SilmataivasWeb do
- pipe_through :api_public
-
- get "/health", HealthController, :index
- end
-
- # Enable LiveDashboard and Swoosh mailbox preview in development
- if Application.compile_env(:silmataivas, :dev_routes) do
- # If you want to use the LiveDashboard in production, you should put
- # it behind authentication and allow only admins to access it.
- # If your application does not have an admins-only section yet,
- # you can use Plug.BasicAuth to set up some basic authentication
- # as long as you are also using SSL (which you should anyway).
- import Phoenix.LiveDashboard.Router
-
- scope "/dev" do
- pipe_through [:fetch_session, :protect_from_forgery]
-
- live_dashboard "/dashboard", metrics: SilmataivasWeb.Telemetry
- forward "/mailbox", Plug.Swoosh.MailboxPreview
- end
- end
-end
diff --git a/lib/silmataivas_web/telemetry.ex b/lib/silmataivas_web/telemetry.ex
deleted file mode 100644
index f893b0e..0000000
--- a/lib/silmataivas_web/telemetry.ex
+++ /dev/null
@@ -1,93 +0,0 @@
-defmodule SilmataivasWeb.Telemetry do
- use Supervisor
- import Telemetry.Metrics
-
- def start_link(arg) do
- Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
- end
-
- @impl true
- def init(_arg) do
- children = [
- # Telemetry poller will execute the given period measurements
- # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
- {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
- # Add reporters as children of your supervision tree.
- # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
- ]
-
- Supervisor.init(children, strategy: :one_for_one)
- end
-
- def metrics do
- [
- # Phoenix Metrics
- summary("phoenix.endpoint.start.system_time",
- unit: {:native, :millisecond}
- ),
- summary("phoenix.endpoint.stop.duration",
- unit: {:native, :millisecond}
- ),
- summary("phoenix.router_dispatch.start.system_time",
- tags: [:route],
- unit: {:native, :millisecond}
- ),
- summary("phoenix.router_dispatch.exception.duration",
- tags: [:route],
- unit: {:native, :millisecond}
- ),
- summary("phoenix.router_dispatch.stop.duration",
- tags: [:route],
- unit: {:native, :millisecond}
- ),
- summary("phoenix.socket_connected.duration",
- unit: {:native, :millisecond}
- ),
- sum("phoenix.socket_drain.count"),
- summary("phoenix.channel_joined.duration",
- unit: {:native, :millisecond}
- ),
- summary("phoenix.channel_handled_in.duration",
- tags: [:event],
- unit: {:native, :millisecond}
- ),
-
- # Database Metrics
- summary("silmataivas.repo.query.total_time",
- unit: {:native, :millisecond},
- description: "The sum of the other measurements"
- ),
- summary("silmataivas.repo.query.decode_time",
- unit: {:native, :millisecond},
- description: "The time spent decoding the data received from the database"
- ),
- summary("silmataivas.repo.query.query_time",
- unit: {:native, :millisecond},
- description: "The time spent executing the query"
- ),
- summary("silmataivas.repo.query.queue_time",
- unit: {:native, :millisecond},
- description: "The time spent waiting for a database connection"
- ),
- summary("silmataivas.repo.query.idle_time",
- unit: {:native, :millisecond},
- description:
- "The time the connection spent waiting before being checked out for the query"
- ),
-
- # VM Metrics
- summary("vm.memory.total", unit: {:byte, :kilobyte}),
- summary("vm.total_run_queue_lengths.total"),
- summary("vm.total_run_queue_lengths.cpu"),
- summary("vm.total_run_queue_lengths.io")
- ]
- end
-
- defp periodic_measurements do
- [
- # A module, function and arguments to be invoked periodically.
- # This function must call :telemetry.execute/3 and a metric must be added above.
- # {SilmataivasWeb, :count_users, []}
- ]
- end
-end
diff --git a/migrations/20240101000000_create_users.sql b/migrations/20240101000000_create_users.sql
new file mode 100644
index 0000000..8519916
--- /dev/null
+++ b/migrations/20240101000000_create_users.sql
@@ -0,0 +1,6 @@
+-- Migration: Create users table
+CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL UNIQUE,
+ role TEXT NOT NULL DEFAULT 'user'
+); \ No newline at end of file
diff --git a/migrations/20240101000100_create_locations.sql b/migrations/20240101000100_create_locations.sql
new file mode 100644
index 0000000..501fb10
--- /dev/null
+++ b/migrations/20240101000100_create_locations.sql
@@ -0,0 +1,9 @@
+-- Migration: Create locations table
+CREATE TABLE IF NOT EXISTS locations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ latitude REAL NOT NULL,
+ longitude REAL NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE NO ACTION
+);
+CREATE INDEX IF NOT EXISTS idx_locations_user_id ON locations(user_id); \ No newline at end of file
diff --git a/migrations/20240101000200_create_weather_thresholds.sql b/migrations/20240101000200_create_weather_thresholds.sql
new file mode 100644
index 0000000..d6ebf0d
--- /dev/null
+++ b/migrations/20240101000200_create_weather_thresholds.sql
@@ -0,0 +1,13 @@
+-- Migration: Create weather_thresholds table
+CREATE TABLE IF NOT EXISTS weather_thresholds (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ condition_type TEXT NOT NULL,
+ threshold_value REAL NOT NULL,
+ operator TEXT NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT 1,
+ description TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+CREATE INDEX IF NOT EXISTS idx_weather_thresholds_user_id ON weather_thresholds(user_id);
+CREATE INDEX IF NOT EXISTS idx_weather_thresholds_condition_type ON weather_thresholds(condition_type); \ No newline at end of file
diff --git a/migrations/20240101000300_create_user_notification_settings.sql b/migrations/20240101000300_create_user_notification_settings.sql
new file mode 100644
index 0000000..13d45cd
--- /dev/null
+++ b/migrations/20240101000300_create_user_notification_settings.sql
@@ -0,0 +1,32 @@
+-- Migration: Create user_ntfy_settings table
+CREATE TABLE IF NOT EXISTS user_ntfy_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT 0,
+ topic TEXT NOT NULL,
+ server_url TEXT NOT NULL,
+ priority INTEGER NOT NULL DEFAULT 5,
+ title_template TEXT,
+ message_template TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_user_ntfy_settings_user_id ON user_ntfy_settings(user_id);
+
+-- Migration: Create user_smtp_settings table
+CREATE TABLE IF NOT EXISTS user_smtp_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT 0,
+ email TEXT NOT NULL,
+ smtp_server TEXT NOT NULL,
+ smtp_port INTEGER NOT NULL,
+ username TEXT,
+ password TEXT,
+ use_tls BOOLEAN NOT NULL DEFAULT 1,
+ from_email TEXT,
+ from_name TEXT DEFAULT 'Silmätaivas Alerts',
+ subject_template TEXT,
+ body_template TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_user_smtp_settings_user_id ON user_smtp_settings(user_id); \ No newline at end of file
diff --git a/mix.exs b/mix.exs
deleted file mode 100644
index fd061c5..0000000
--- a/mix.exs
+++ /dev/null
@@ -1,77 +0,0 @@
-defmodule Silmataivas.MixProject do
- use Mix.Project
-
- def project do
- [
- app: :silmataivas,
- version: "0.1.0",
- elixir: "~> 1.14",
- elixirc_paths: elixirc_paths(Mix.env()),
- start_permanent: Mix.env() == :prod,
- aliases: aliases(),
- deps: deps()
- ]
- end
-
- # Configuration for the OTP application.
- #
- # Type `mix help compile.app` for more information.
- def application do
- [
- mod: {Silmataivas.Application, []},
- extra_applications: [:logger, :runtime_tools]
- ]
- end
-
- # Specifies which paths to compile per environment.
- defp elixirc_paths(:test), do: ["lib", "test/support"]
- defp elixirc_paths(_), do: ["lib"]
-
- # Specifies your project dependencies.
- #
- # Type `mix help deps` for examples and options.
- defp deps do
- [
- {:phoenix, "~> 1.7.20"},
- {:phoenix_ecto, "~> 4.5"},
- {:ecto_sql, "~> 3.10"},
- # Database adapters
- # SQLite support
- {:ecto_sqlite3, "~> 0.19.0"},
- # PostgreSQL support
- {:postgrex, ">= 0.0.0"},
- # Other dependencies
- {:phoenix_live_dashboard, "~> 0.8.3"},
- {:swoosh, "~> 1.18.3"},
- {:finch, "~> 0.13"},
- {:telemetry_metrics, "~> 1.0"},
- {:telemetry_poller, "~> 1.0"},
- {:gettext, "~> 0.26"},
- {:jason, "~> 1.2"},
- {:dns_cluster, "~> 0.2"},
- {:bandit, "~> 1.5"},
- {:hackney, "~> 1.9"},
- # HTTP client
- {:req, "~> 0.5.10"},
- # Scheduler
- {:quantum, "~> 3.5"},
- # SMTP adapter for Swoosh
- {:gen_smtp, "~> 1.2"}
- ]
- end
-
- # Aliases are shortcuts or tasks specific to the current project.
- # For example, to install project dependencies and perform other setup tasks, run:
- #
- # $ mix setup
- #
- # See the documentation for `Mix` for more info on aliases.
- defp aliases do
- [
- setup: ["deps.get", "ecto.setup"],
- "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
- "ecto.reset": ["ecto.drop", "ecto.setup"],
- test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
- ]
- end
-end
diff --git a/mix.lock b/mix.lock
deleted file mode 100644
index 4a48d09..0000000
--- a/mix.lock
+++ /dev/null
@@ -1,63 +0,0 @@
-%{
- "artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"},
- "bandit": {:hex, :bandit, "1.6.9", "cf4653d0490941629a4475381eda3b8d4d2653471a9efe0147b2195bef40ece5", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "67ab91338f308da9fb10d5afde35899e15af653adf31d682dd3a0e7c1d34db23"},
- "bootleg": {:hex, :bootleg, "0.13.0", "2fadc6ad67940637d9d305e35e80a75e7a936d1a570cc5146c8bd1f6ddb63280", [:mix], [{:distillery, ">= 2.1.0", [hex: :distillery, repo: "hexpm", optional: false]}, {:ssh_client_key_api, "~> 0.2.1", [hex: :ssh_client_key_api, repo: "hexpm", optional: false]}, {:sshkit, "0.3.0", [hex: :sshkit, repo: "hexpm", optional: false]}], "hexpm", "deccf4f78e4b9decc2a24be29c253e48ef481f3f816adfbdc73bdfbb204b6aa8"},
- "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
- "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
- "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
- "crontab": {:hex, :crontab, "1.1.14", "233fcfdc2c74510cabdbcb800626babef414e7cb13cea11ddf62e10e16e2bf76", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "4e3b9950bc22ae8d0395ffb5f4b127a140005cba95745abf5ff9ee7e8203c6fa"},
- "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
- "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
- "distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"},
- "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
- "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
- "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
- "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.19.0", "00030bbaba150369ff3754bbc0d2c28858e8f528ae406bf6997d1772d3a03203", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "297b16750fe229f3056fe32afd3247de308094e8b0298aef0d73a8493ce97c81"},
- "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
- "ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},
- "ex_aws_ses": {:hex, :ex_aws_ses, "2.4.1", "1aa945610121c9891054c27d0f71f5799b2e0a2062044d742d89c1cee251f9e2", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "dddac42d4d7b826f7099bbe7402a35e68eb76434d6c58bfa332002ea2b522645"},
- "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
- "exqlite": {:hex, :exqlite, "0.29.0", "e6f1de4bfe3ce6e4c4260b15fef830705fa36632218dc7eafa0a5aba3a5d6e04", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a75f8a069fcdad3e5f95dfaddccd13c2112ea3b742fdcc234b96410e9c1bde00"},
- "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
- "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
- "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
- "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
- "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
- "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
- "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
- "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
- "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
- "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
- "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
- "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
- "nimble_ownership": {:hex, :nimble_ownership, "0.3.2", "d4fa4056ade0ae33b5a9eb64554a1b3779689282e37513260125d2d6b32e4874", [:mix], [], "hexpm", "28b9a9f4094fda1aa8ca72f732ff3223eb54aa3eda4fed9022254de2c152b138"},
- "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
- "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
- "phoenix": {:hex, :phoenix, "1.7.20", "6bababaf27d59f5628f9b608de902a021be2cecefb8231e1dbdc0a2e2e480e9b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6be2ab98302e8784a31829e0d50d8bdfa81a23cd912c395bafd8b8bfb5a086c2"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
- "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
- "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"},
- "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.7", "491c5fcccb9cee4978a25f0ec4c4b01975cd5f8d6d2366ca1bd5bf6f7f81a862", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a1758c5816f65c83af38dfeef35a6d491363e32c707c2e3bb6b8f6339e8f2cbf"},
- "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
- "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
- "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
- "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
- "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
- "quantum": {:hex, :quantum, "3.5.3", "ee38838a07761663468145f489ad93e16a79440bebd7c0f90dc1ec9850776d99", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "500fd3fa77dcd723ed9f766d4a175b684919ff7b6b8cfd9d7d0564d58eba8734"},
- "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
- "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
- "ssh_client_key_api": {:git, "https://github.com/axelson/ssh_client_key_api.git", "ad7ba753b1049bb13c1a9115ced0584531abca6a", [branch: "support-erlang-otp-25"]},
- "sshkit": {:hex, :sshkit, "0.3.0", "4c100e3c3ebd261b6b7de811ade713f425fb06eb730a96d583da18d29a6fca26", [:mix], [], "hexpm", "f5dba2ee21e2ddc7c1432e3329ecf7316a1032e9ce911597e1e56823ee10285c"},
- "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
- "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
- "swoosh": {:hex, :swoosh, "1.18.3", "ca12197550bd7456654179055b1446168cc0f55067f784a3707e0e4462e269f5", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a533daccea84e887a061a919295212b37f4f2c7916436037eb8be7f1265bacba"},
- "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
- "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
- "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
- "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"},
- "thousand_island": {:hex, :thousand_island, "1.3.12", "590ff651a6d2a59ed7eabea398021749bdc664e2da33e0355e6c64e7e1a2ef93", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "55d0b1c868b513a7225892b8a8af0234d7c8981a51b0740369f3125f7c99a549"},
- "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
- "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
- "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
-}
diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po
deleted file mode 100644
index 844c4f5..0000000
--- a/priv/gettext/en/LC_MESSAGES/errors.po
+++ /dev/null
@@ -1,112 +0,0 @@
-## `msgid`s in this file come from POT (.pot) files.
-##
-## Do not add, change, or remove `msgid`s manually here as
-## they're tied to the ones in the corresponding POT file
-## (with the same domain).
-##
-## Use `mix gettext.extract --merge` or `mix gettext.merge`
-## to merge POT files into PO files.
-msgid ""
-msgstr ""
-"Language: en\n"
-
-## From Ecto.Changeset.cast/4
-msgid "can't be blank"
-msgstr ""
-
-## From Ecto.Changeset.unique_constraint/3
-msgid "has already been taken"
-msgstr ""
-
-## From Ecto.Changeset.put_change/3
-msgid "is invalid"
-msgstr ""
-
-## From Ecto.Changeset.validate_acceptance/3
-msgid "must be accepted"
-msgstr ""
-
-## From Ecto.Changeset.validate_format/3
-msgid "has invalid format"
-msgstr ""
-
-## From Ecto.Changeset.validate_subset/3
-msgid "has an invalid entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_exclusion/3
-msgid "is reserved"
-msgstr ""
-
-## From Ecto.Changeset.validate_confirmation/3
-msgid "does not match confirmation"
-msgstr ""
-
-## From Ecto.Changeset.no_assoc_constraint/3
-msgid "is still associated with this entry"
-msgstr ""
-
-msgid "are still associated with this entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_length/3
-msgid "should have %{count} item(s)"
-msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be %{count} character(s)"
-msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be %{count} byte(s)"
-msgid_plural "should be %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at least %{count} item(s)"
-msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at least %{count} character(s)"
-msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at least %{count} byte(s)"
-msgid_plural "should be at least %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at most %{count} item(s)"
-msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at most %{count} character(s)"
-msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at most %{count} byte(s)"
-msgid_plural "should be at most %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-## From Ecto.Changeset.validate_number/3
-msgid "must be less than %{number}"
-msgstr ""
-
-msgid "must be greater than %{number}"
-msgstr ""
-
-msgid "must be less than or equal to %{number}"
-msgstr ""
-
-msgid "must be greater than or equal to %{number}"
-msgstr ""
-
-msgid "must be equal to %{number}"
-msgstr ""
diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot
deleted file mode 100644
index eef2de2..0000000
--- a/priv/gettext/errors.pot
+++ /dev/null
@@ -1,109 +0,0 @@
-## This is a PO Template file.
-##
-## `msgid`s here are often extracted from source code.
-## Add new translations manually only if they're dynamic
-## translations that can't be statically extracted.
-##
-## Run `mix gettext.extract` to bring this file up to
-## date. Leave `msgstr`s empty as changing them here has no
-## effect: edit them in PO (`.po`) files instead.
-## From Ecto.Changeset.cast/4
-msgid "can't be blank"
-msgstr ""
-
-## From Ecto.Changeset.unique_constraint/3
-msgid "has already been taken"
-msgstr ""
-
-## From Ecto.Changeset.put_change/3
-msgid "is invalid"
-msgstr ""
-
-## From Ecto.Changeset.validate_acceptance/3
-msgid "must be accepted"
-msgstr ""
-
-## From Ecto.Changeset.validate_format/3
-msgid "has invalid format"
-msgstr ""
-
-## From Ecto.Changeset.validate_subset/3
-msgid "has an invalid entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_exclusion/3
-msgid "is reserved"
-msgstr ""
-
-## From Ecto.Changeset.validate_confirmation/3
-msgid "does not match confirmation"
-msgstr ""
-
-## From Ecto.Changeset.no_assoc_constraint/3
-msgid "is still associated with this entry"
-msgstr ""
-
-msgid "are still associated with this entry"
-msgstr ""
-
-## From Ecto.Changeset.validate_length/3
-msgid "should have %{count} item(s)"
-msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be %{count} character(s)"
-msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be %{count} byte(s)"
-msgid_plural "should be %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at least %{count} item(s)"
-msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at least %{count} character(s)"
-msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at least %{count} byte(s)"
-msgid_plural "should be at least %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should have at most %{count} item(s)"
-msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at most %{count} character(s)"
-msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "should be at most %{count} byte(s)"
-msgid_plural "should be at most %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
-
-## From Ecto.Changeset.validate_number/3
-msgid "must be less than %{number}"
-msgstr ""
-
-msgid "must be greater than %{number}"
-msgstr ""
-
-msgid "must be less than or equal to %{number}"
-msgstr ""
-
-msgid "must be greater than or equal to %{number}"
-msgstr ""
-
-msgid "must be equal to %{number}"
-msgstr ""
diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs
deleted file mode 100644
index 49f9151..0000000
--- a/priv/repo/migrations/.formatter.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-[
- import_deps: [:ecto_sql],
- inputs: ["*.exs"]
-]
diff --git a/priv/repo/migrations/20250323093704_create_users.exs b/priv/repo/migrations/20250323093704_create_users.exs
deleted file mode 100644
index c418326..0000000
--- a/priv/repo/migrations/20250323093704_create_users.exs
+++ /dev/null
@@ -1,13 +0,0 @@
-defmodule Silmataivas.Repo.Migrations.CreateUsers do
- use Ecto.Migration
-
- def change do
- create table(:users) do
- add :user_id, :string
-
- timestamps(type: :utc_datetime)
- end
-
- create unique_index(:users, [:user_id])
- end
-end
diff --git a/priv/repo/migrations/20250323093713_create_locations.exs b/priv/repo/migrations/20250323093713_create_locations.exs
deleted file mode 100644
index 9373024..0000000
--- a/priv/repo/migrations/20250323093713_create_locations.exs
+++ /dev/null
@@ -1,15 +0,0 @@
-defmodule Silmataivas.Repo.Migrations.CreateLocations do
- use Ecto.Migration
-
- def change do
- create table(:locations) do
- add :latitude, :float
- add :longitude, :float
- add :user_id, references(:users, on_delete: :nothing)
-
- timestamps(type: :utc_datetime)
- end
-
- create index(:locations, [:user_id])
- end
-end
diff --git a/priv/repo/migrations/20250326104054_add_role_to_users.exs b/priv/repo/migrations/20250326104054_add_role_to_users.exs
deleted file mode 100644
index 786b46f..0000000
--- a/priv/repo/migrations/20250326104054_add_role_to_users.exs
+++ /dev/null
@@ -1,9 +0,0 @@
-defmodule Silmataivas.Repo.Migrations.AddRoleToUsers do
- use Ecto.Migration
-
- def change do
- alter table(:users) do
- add :role, :string, default: "user"
- end
- end
-end
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
deleted file mode 100644
index 1a102d7..0000000
--- a/priv/repo/seeds.exs
+++ /dev/null
@@ -1,11 +0,0 @@
-# Script for populating the database. You can run it as:
-#
-# mix run priv/repo/seeds.exs
-#
-# Inside the script, you can read and write to any of your
-# repositories directly:
-#
-# Silmataivas.Repo.insert!(%Silmataivas.SomeSchema{})
-#
-# We recommend using the bang functions (`insert!`, `update!`
-# and so on) as they will fail if something goes wrong.
diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico
deleted file mode 100644
index 7f372bf..0000000
--- a/priv/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/priv/static/robots.txt b/priv/static/robots.txt
deleted file mode 100644
index 26e06b5..0000000
--- a/priv/static/robots.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
-#
-# To ban all spiders from the entire site uncomment the next two lines:
-# User-agent: *
-# Disallow: /
diff --git a/src/health.rs b/src/health.rs
new file mode 100644
index 0000000..daa2f43
--- /dev/null
+++ b/src/health.rs
@@ -0,0 +1,6 @@
+use axum::{response::IntoResponse, Json};
+use serde_json::json;
+
+pub async fn health_handler() -> impl IntoResponse {
+ Json(json!({"status": "ok"}))
+} \ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..f51483b
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,31 @@
+use axum::{Router, routing::get};
+
+pub fn app() -> Router {
+ Router::new()
+ .route("/health", get(crate::health::health_handler))
+}
+
+pub mod users;
+pub mod locations;
+pub mod weather_thresholds;
+pub mod notifications;
+pub mod weather_poller;
+pub mod health;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use axum::body::Body;
+ use axum::http::{Request, StatusCode};
+ use tower::ServiceExt; // for `oneshot`
+ use axum::body::to_bytes;
+
+ #[tokio::test]
+ async fn test_health_endpoint() {
+ let app = app();
+ let response = app.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()).await.unwrap();
+ assert_eq!(response.status(), StatusCode::OK);
+ let body = to_bytes(response.into_body(), 1024).await.unwrap();
+ assert_eq!(&body[..], b"{\"status\":\"ok\"}");
+ }
+}
diff --git a/src/locations.rs b/src/locations.rs
new file mode 100644
index 0000000..e5f881d
--- /dev/null
+++ b/src/locations.rs
@@ -0,0 +1,139 @@
+use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
+
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+pub struct Location {
+ pub id: i64,
+ pub latitude: f64,
+ pub longitude: f64,
+ pub user_id: i64,
+}
+
+pub struct LocationRepository<'a> {
+ pub db: &'a sqlx::SqlitePool,
+}
+
+impl<'a> LocationRepository<'a> {
+ pub async fn list_locations(&self) -> Result<Vec<Location>, sqlx::Error> {
+ sqlx::query_as::<_, Location>("SELECT id, latitude, longitude, user_id FROM locations")
+ .fetch_all(self.db)
+ .await
+ }
+
+ pub async fn get_location(&self, id: i64) -> Result<Option<Location>, sqlx::Error> {
+ sqlx::query_as::<_, Location>("SELECT id, latitude, longitude, user_id FROM locations WHERE id = ?")
+ .bind(id)
+ .fetch_optional(self.db)
+ .await
+ }
+
+ pub async fn create_location(&self, latitude: f64, longitude: f64, user_id: i64) -> Result<Location, sqlx::Error> {
+ sqlx::query_as::<_, Location>(
+ "INSERT INTO locations (latitude, longitude, user_id) VALUES (?, ?, ?) RETURNING id, latitude, longitude, user_id"
+ )
+ .bind(latitude)
+ .bind(longitude)
+ .bind(user_id)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn update_location(&self, id: i64, latitude: f64, longitude: f64) -> Result<Location, sqlx::Error> {
+ sqlx::query_as::<_, Location>(
+ "UPDATE locations SET latitude = ?, longitude = ? WHERE id = ? RETURNING id, latitude, longitude, user_id"
+ )
+ .bind(latitude)
+ .bind(longitude)
+ .bind(id)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn delete_location(&self, id: i64) -> Result<(), sqlx::Error> {
+ sqlx::query("DELETE FROM locations WHERE id = ?")
+ .bind(id)
+ .execute(self.db)
+ .await?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::users::{UserRepository, UserRole};
+ use sqlx::{SqlitePool, Executor};
+ use tokio;
+
+ async fn setup_db() -> SqlitePool {
+ let pool = SqlitePool::connect(":memory:").await.unwrap();
+ pool.execute(
+ "CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL UNIQUE,
+ role TEXT NOT NULL DEFAULT 'user'
+ );"
+ ).await.unwrap();
+ pool.execute(
+ "CREATE TABLE locations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ latitude REAL NOT NULL,
+ longitude REAL NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE NO ACTION
+ );"
+ ).await.unwrap();
+ pool
+ }
+
+ async fn create_user(pool: &SqlitePool) -> i64 {
+ let repo = UserRepository { db: pool };
+ let user = repo.create_user(None, Some(UserRole::User)).await.unwrap();
+ user.id
+ }
+
+ #[tokio::test]
+ async fn test_create_and_get_location() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = LocationRepository { db: &db };
+ let loc = repo.create_location(60.0, 24.0, user_id).await.unwrap();
+ let fetched = repo.get_location(loc.id).await.unwrap().unwrap();
+ assert_eq!(fetched.latitude, 60.0);
+ assert_eq!(fetched.longitude, 24.0);
+ assert_eq!(fetched.user_id, user_id);
+ }
+
+ #[tokio::test]
+ async fn test_update_location() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = LocationRepository { db: &db };
+ let loc = repo.create_location(60.0, 24.0, user_id).await.unwrap();
+ let updated = repo.update_location(loc.id, 61.0, 25.0).await.unwrap();
+ assert_eq!(updated.latitude, 61.0);
+ assert_eq!(updated.longitude, 25.0);
+ }
+
+ #[tokio::test]
+ async fn test_delete_location() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = LocationRepository { db: &db };
+ let loc = repo.create_location(60.0, 24.0, user_id).await.unwrap();
+ repo.delete_location(loc.id).await.unwrap();
+ let fetched = repo.get_location(loc.id).await.unwrap();
+ assert!(fetched.is_none());
+ }
+
+ #[tokio::test]
+ async fn test_list_locations() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = LocationRepository { db: &db };
+ repo.create_location(60.0, 24.0, user_id).await.unwrap();
+ repo.create_location(61.0, 25.0, user_id).await.unwrap();
+ let locations = repo.list_locations().await.unwrap();
+ assert_eq!(locations.len(), 2);
+ }
+} \ No newline at end of file
diff --git a/src/notifications.rs b/src/notifications.rs
new file mode 100644
index 0000000..fb49e97
--- /dev/null
+++ b/src/notifications.rs
@@ -0,0 +1,294 @@
+use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
+
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+pub struct NtfySettings {
+ pub id: i64,
+ pub user_id: i64,
+ pub enabled: bool,
+ pub topic: String,
+ pub server_url: String,
+ pub priority: i32,
+ pub title_template: Option<String>,
+ pub message_template: Option<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+pub struct SmtpSettings {
+ pub id: i64,
+ pub user_id: i64,
+ pub enabled: bool,
+ pub email: String,
+ pub smtp_server: String,
+ pub smtp_port: i32,
+ pub username: Option<String>,
+ pub password: Option<String>,
+ pub use_tls: bool,
+ pub from_email: Option<String>,
+ pub from_name: Option<String>,
+ pub subject_template: Option<String>,
+ pub body_template: Option<String>,
+}
+
+pub struct NtfySettingsRepository<'a> {
+ pub db: &'a sqlx::SqlitePool,
+}
+
+impl<'a> NtfySettingsRepository<'a> {
+ pub async fn get_by_user(&self, user_id: i64) -> Result<Option<NtfySettings>, sqlx::Error> {
+ sqlx::query_as::<_, NtfySettings>(
+ "SELECT * FROM user_ntfy_settings WHERE user_id = ?"
+ )
+ .bind(user_id)
+ .fetch_optional(self.db)
+ .await
+ }
+
+ pub async fn create(&self, user_id: i64, enabled: bool, topic: String, server_url: String, priority: i32, title_template: Option<String>, message_template: Option<String>) -> Result<NtfySettings, sqlx::Error> {
+ sqlx::query_as::<_, NtfySettings>(
+ "INSERT INTO user_ntfy_settings (user_id, enabled, topic, server_url, priority, title_template, message_template) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *"
+ )
+ .bind(user_id)
+ .bind(enabled)
+ .bind(topic)
+ .bind(server_url)
+ .bind(priority)
+ .bind(title_template)
+ .bind(message_template)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn update(&self, id: i64, enabled: bool, topic: String, server_url: String, priority: i32, title_template: Option<String>, message_template: Option<String>) -> Result<NtfySettings, sqlx::Error> {
+ sqlx::query_as::<_, NtfySettings>(
+ "UPDATE user_ntfy_settings SET enabled = ?, topic = ?, server_url = ?, priority = ?, title_template = ?, message_template = ? WHERE id = ? RETURNING *"
+ )
+ .bind(enabled)
+ .bind(topic)
+ .bind(server_url)
+ .bind(priority)
+ .bind(title_template)
+ .bind(message_template)
+ .bind(id)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn delete(&self, id: i64) -> Result<(), sqlx::Error> {
+ sqlx::query("DELETE FROM user_ntfy_settings WHERE id = ?")
+ .bind(id)
+ .execute(self.db)
+ .await?;
+ Ok(())
+ }
+}
+
+pub struct SmtpSettingsRepository<'a> {
+ pub db: &'a sqlx::SqlitePool,
+}
+
+impl<'a> SmtpSettingsRepository<'a> {
+ pub async fn get_by_user(&self, user_id: i64) -> Result<Option<SmtpSettings>, sqlx::Error> {
+ sqlx::query_as::<_, SmtpSettings>(
+ "SELECT * FROM user_smtp_settings WHERE user_id = ?"
+ )
+ .bind(user_id)
+ .fetch_optional(self.db)
+ .await
+ }
+
+ pub async fn create(&self, user_id: i64, enabled: bool, email: String, smtp_server: String, smtp_port: i32, username: Option<String>, password: Option<String>, use_tls: bool, from_email: Option<String>, from_name: Option<String>, subject_template: Option<String>, body_template: Option<String>) -> Result<SmtpSettings, sqlx::Error> {
+ sqlx::query_as::<_, SmtpSettings>(
+ "INSERT INTO user_smtp_settings (user_id, enabled, email, smtp_server, smtp_port, username, password, use_tls, from_email, from_name, subject_template, body_template) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *"
+ )
+ .bind(user_id)
+ .bind(enabled)
+ .bind(email)
+ .bind(smtp_server)
+ .bind(smtp_port)
+ .bind(username)
+ .bind(password)
+ .bind(use_tls)
+ .bind(from_email)
+ .bind(from_name)
+ .bind(subject_template)
+ .bind(body_template)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn update(&self, id: i64, enabled: bool, email: String, smtp_server: String, smtp_port: i32, username: Option<String>, password: Option<String>, use_tls: bool, from_email: Option<String>, from_name: Option<String>, subject_template: Option<String>, body_template: Option<String>) -> Result<SmtpSettings, sqlx::Error> {
+ sqlx::query_as::<_, SmtpSettings>(
+ "UPDATE user_smtp_settings SET enabled = ?, email = ?, smtp_server = ?, smtp_port = ?, username = ?, password = ?, use_tls = ?, from_email = ?, from_name = ?, subject_template = ?, body_template = ? WHERE id = ? RETURNING *"
+ )
+ .bind(enabled)
+ .bind(email)
+ .bind(smtp_server)
+ .bind(smtp_port)
+ .bind(username)
+ .bind(password)
+ .bind(use_tls)
+ .bind(from_email)
+ .bind(from_name)
+ .bind(subject_template)
+ .bind(body_template)
+ .bind(id)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn delete(&self, id: i64) -> Result<(), sqlx::Error> {
+ sqlx::query("DELETE FROM user_smtp_settings WHERE id = ?")
+ .bind(id)
+ .execute(self.db)
+ .await?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::users::{UserRepository, UserRole};
+ use sqlx::{SqlitePool, Executor};
+ use tokio;
+
+ async fn setup_db() -> SqlitePool {
+ let pool = SqlitePool::connect(":memory:").await.unwrap();
+ pool.execute(
+ "CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL UNIQUE,
+ role TEXT NOT NULL DEFAULT 'user'
+ );"
+ ).await.unwrap();
+ pool.execute(
+ "CREATE TABLE user_ntfy_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT 0,
+ topic TEXT NOT NULL,
+ server_url TEXT NOT NULL,
+ priority INTEGER NOT NULL DEFAULT 5,
+ title_template TEXT,
+ message_template TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ );"
+ ).await.unwrap();
+ pool.execute(
+ "CREATE TABLE user_smtp_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT 0,
+ email TEXT NOT NULL,
+ smtp_server TEXT NOT NULL,
+ smtp_port INTEGER NOT NULL,
+ username TEXT,
+ password TEXT,
+ use_tls BOOLEAN NOT NULL DEFAULT 1,
+ from_email TEXT,
+ from_name TEXT DEFAULT 'Silmätaivas Alerts',
+ subject_template TEXT,
+ body_template TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ );"
+ ).await.unwrap();
+ pool
+ }
+
+ async fn create_user(pool: &SqlitePool) -> i64 {
+ let repo = UserRepository { db: pool };
+ let user = repo.create_user(None, Some(UserRole::User)).await.unwrap();
+ user.id
+ }
+
+ #[tokio::test]
+ async fn test_create_and_get_ntfy_settings() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = NtfySettingsRepository { db: &db };
+ let settings = repo.create(user_id, true, "topic1".to_string(), "https://ntfy.sh".to_string(), 3, Some("title".to_string()), Some("msg".to_string())).await.unwrap();
+ let fetched = repo.get_by_user(user_id).await.unwrap().unwrap();
+ assert_eq!(fetched.topic, "topic1");
+ assert_eq!(fetched.server_url, "https://ntfy.sh");
+ assert_eq!(fetched.priority, 3);
+ assert_eq!(fetched.title_template, Some("title".to_string()));
+ assert_eq!(fetched.message_template, Some("msg".to_string()));
+ }
+
+ #[tokio::test]
+ async fn test_update_ntfy_settings() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = NtfySettingsRepository { db: &db };
+ let settings = repo.create(user_id, true, "topic1".to_string(), "https://ntfy.sh".to_string(), 3, None, None).await.unwrap();
+ let updated = repo.update(settings.id, false, "topic2".to_string(), "https://ntfy2.sh".to_string(), 4, Some("t2".to_string()), Some("m2".to_string())).await.unwrap();
+ assert_eq!(updated.enabled, false);
+ assert_eq!(updated.topic, "topic2");
+ assert_eq!(updated.server_url, "https://ntfy2.sh");
+ assert_eq!(updated.priority, 4);
+ assert_eq!(updated.title_template, Some("t2".to_string()));
+ assert_eq!(updated.message_template, Some("m2".to_string()));
+ }
+
+ #[tokio::test]
+ async fn test_delete_ntfy_settings() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = NtfySettingsRepository { db: &db };
+ let settings = repo.create(user_id, true, "topic1".to_string(), "https://ntfy.sh".to_string(), 3, None, None).await.unwrap();
+ repo.delete(settings.id).await.unwrap();
+ let fetched = repo.get_by_user(user_id).await.unwrap();
+ assert!(fetched.is_none());
+ }
+
+ #[tokio::test]
+ async fn test_create_and_get_smtp_settings() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = SmtpSettingsRepository { db: &db };
+ let settings = repo.create(user_id, true, "test@example.com".to_string(), "smtp.example.com".to_string(), 587, Some("user".to_string()), Some("pass".to_string()), true, Some("from@example.com".to_string()), Some("Alerts".to_string()), Some("subj".to_string()), Some("body".to_string())).await.unwrap();
+ let fetched = repo.get_by_user(user_id).await.unwrap().unwrap();
+ assert_eq!(fetched.email, "test@example.com");
+ assert_eq!(fetched.smtp_server, "smtp.example.com");
+ assert_eq!(fetched.smtp_port, 587);
+ assert_eq!(fetched.username, Some("user".to_string()));
+ assert_eq!(fetched.password, Some("pass".to_string()));
+ assert_eq!(fetched.use_tls, true);
+ assert_eq!(fetched.from_email, Some("from@example.com".to_string()));
+ assert_eq!(fetched.from_name, Some("Alerts".to_string()));
+ assert_eq!(fetched.subject_template, Some("subj".to_string()));
+ assert_eq!(fetched.body_template, Some("body".to_string()));
+ }
+
+ #[tokio::test]
+ async fn test_update_smtp_settings() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = SmtpSettingsRepository { db: &db };
+ let settings = repo.create(user_id, true, "test@example.com".to_string(), "smtp.example.com".to_string(), 587, None, None, true, None, None, None, None).await.unwrap();
+ let updated = repo.update(settings.id, false, "other@example.com".to_string(), "smtp2.example.com".to_string(), 465, Some("u2".to_string()), Some("p2".to_string()), false, Some("f2@example.com".to_string()), Some("N2".to_string()), Some("s2".to_string()), Some("b2".to_string())).await.unwrap();
+ assert_eq!(updated.enabled, false);
+ assert_eq!(updated.email, "other@example.com");
+ assert_eq!(updated.smtp_server, "smtp2.example.com");
+ assert_eq!(updated.smtp_port, 465);
+ assert_eq!(updated.username, Some("u2".to_string()));
+ assert_eq!(updated.password, Some("p2".to_string()));
+ assert_eq!(updated.use_tls, false);
+ assert_eq!(updated.from_email, Some("f2@example.com".to_string()));
+ assert_eq!(updated.from_name, Some("N2".to_string()));
+ assert_eq!(updated.subject_template, Some("s2".to_string()));
+ assert_eq!(updated.body_template, Some("b2".to_string()));
+ }
+
+ #[tokio::test]
+ async fn test_delete_smtp_settings() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = SmtpSettingsRepository { db: &db };
+ let settings = repo.create(user_id, true, "test@example.com".to_string(), "smtp.example.com".to_string(), 587, None, None, true, None, None, None, None).await.unwrap();
+ repo.delete(settings.id).await.unwrap();
+ let fetched = repo.get_by_user(user_id).await.unwrap();
+ assert!(fetched.is_none());
+ }
+} \ No newline at end of file
diff --git a/src/users.rs b/src/users.rs
new file mode 100644
index 0000000..baca6dd
--- /dev/null
+++ b/src/users.rs
@@ -0,0 +1,139 @@
+use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
+use uuid::Uuid;
+
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, Eq)]
+pub struct User {
+ pub id: i64,
+ pub user_id: String, // API token
+ pub role: UserRole,
+}
+
+#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq, Eq)]
+#[sqlx(type_name = "TEXT")]
+pub enum UserRole {
+ #[serde(rename = "user")]
+ User,
+ #[serde(rename = "admin")]
+ Admin,
+}
+
+impl Default for UserRole {
+ fn default() -> Self {
+ UserRole::User
+ }
+}
+
+pub struct UserRepository<'a> {
+ pub db: &'a sqlx::SqlitePool,
+}
+
+impl<'a> UserRepository<'a> {
+ pub async fn list_users(&self) -> Result<Vec<User>, sqlx::Error> {
+ sqlx::query_as::<_, User>("SELECT id, user_id, role FROM users")
+ .fetch_all(self.db)
+ .await
+ }
+
+ pub async fn get_user_by_id(&self, id: i64) -> Result<Option<User>, sqlx::Error> {
+ sqlx::query_as::<_, User>("SELECT id, user_id, role FROM users WHERE id = ?")
+ .bind(id)
+ .fetch_optional(self.db)
+ .await
+ }
+
+ pub async fn get_user_by_user_id(&self, user_id: &str) -> Result<Option<User>, sqlx::Error> {
+ sqlx::query_as::<_, User>("SELECT id, user_id, role FROM users WHERE user_id = ?")
+ .bind(user_id)
+ .fetch_optional(self.db)
+ .await
+ }
+
+ pub async fn create_user(&self, user_id: Option<String>, role: Option<UserRole>) -> Result<User, sqlx::Error> {
+ let user_id = user_id.unwrap_or_else(|| Uuid::new_v4().to_string());
+ let role = role.unwrap_or_default();
+ sqlx::query_as::<_, User>(
+ "INSERT INTO users (user_id, role) VALUES (?, ?) RETURNING id, user_id, role"
+ )
+ .bind(user_id)
+ .bind(role)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn update_user(&self, id: i64, role: UserRole) -> Result<User, sqlx::Error> {
+ sqlx::query_as::<_, User>(
+ "UPDATE users SET role = ? WHERE id = ? RETURNING id, user_id, role"
+ )
+ .bind(role)
+ .bind(id)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn delete_user(&self, id: i64) -> Result<(), sqlx::Error> {
+ sqlx::query("DELETE FROM users WHERE id = ?")
+ .bind(id)
+ .execute(self.db)
+ .await?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use sqlx::{SqlitePool, Executor};
+ use tokio;
+
+ async fn setup_db() -> SqlitePool {
+ let pool = SqlitePool::connect(":memory:").await.unwrap();
+ pool.execute(
+ "CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL UNIQUE,
+ role TEXT NOT NULL DEFAULT 'user'
+ );"
+ ).await.unwrap();
+ pool
+ }
+
+ #[tokio::test]
+ async fn test_create_and_get_user() {
+ let db = setup_db().await;
+ let repo = UserRepository { db: &db };
+ let user = repo.create_user(None, Some(UserRole::Admin)).await.unwrap();
+ assert_eq!(user.role, UserRole::Admin);
+ let fetched = repo.get_user_by_user_id(&user.user_id).await.unwrap().unwrap();
+ assert_eq!(fetched.user_id, user.user_id);
+ }
+
+ #[tokio::test]
+ async fn test_update_user() {
+ let db = setup_db().await;
+ let repo = UserRepository { db: &db };
+ let user = repo.create_user(None, Some(UserRole::User)).await.unwrap();
+ let updated = repo.update_user(user.id, UserRole::Admin).await.unwrap();
+ assert_eq!(updated.role, UserRole::Admin);
+ }
+
+ #[tokio::test]
+ async fn test_delete_user() {
+ let db = setup_db().await;
+ let repo = UserRepository { db: &db };
+ let user = repo.create_user(None, None).await.unwrap();
+ repo.delete_user(user.id).await.unwrap();
+ let fetched = repo.get_user_by_id(user.id).await.unwrap();
+ assert!(fetched.is_none());
+ }
+
+ #[tokio::test]
+ async fn test_list_users() {
+ let db = setup_db().await;
+ let repo = UserRepository { db: &db };
+ repo.create_user(None, Some(UserRole::User)).await.unwrap();
+ repo.create_user(None, Some(UserRole::Admin)).await.unwrap();
+ let users = repo.list_users().await.unwrap();
+ assert_eq!(users.len(), 2);
+ }
+} \ No newline at end of file
diff --git a/src/weather_poller.rs b/src/weather_poller.rs
new file mode 100644
index 0000000..056cef8
--- /dev/null
+++ b/src/weather_poller.rs
@@ -0,0 +1,192 @@
+use crate::users::UserRepository;
+use crate::locations::LocationRepository;
+use crate::weather_thresholds::{WeatherThresholdRepository, WeatherThreshold};
+use crate::notifications::{NtfySettingsRepository, SmtpSettingsRepository, NtfySettings, SmtpSettings};
+use serde_json::Value;
+use tera::{Tera, Context};
+use reqwest::Client;
+use std::sync::{Arc, Mutex};
+use tokio_task_scheduler::{Scheduler, Task};
+use tokio::time::Duration;
+
+const OWM_API_URL: &str = "https://api.openweathermap.org/data/2.5/forecast";
+const ALERT_WINDOW_HOURS: i64 = 24;
+
+pub struct WeatherPoller {
+ pub db: Arc<sqlx::SqlitePool>,
+ pub owm_api_key: String,
+ pub tera: Arc<Mutex<Tera>>,
+}
+
+impl WeatherPoller {
+ pub async fn check_all(&self) {
+ let user_repo = UserRepository { db: &self.db };
+ let loc_repo = LocationRepository { db: &self.db };
+ let users = user_repo.list_users().await.unwrap_or_default();
+ for user in users {
+ if let Some(location) = loc_repo.list_locations().await.unwrap_or_default().into_iter().find(|l| l.user_id == user.id) {
+ self.check_user_weather(user.id, location.latitude, location.longitude).await;
+ }
+ }
+ }
+
+ pub async fn check_user_weather(&self, user_id: i64, lat: f64, lon: f64) {
+ if let Ok(Some(forecast)) = self.fetch_forecast(lat, lon).await {
+ let threshold_repo = WeatherThresholdRepository { db: &self.db };
+ let thresholds = threshold_repo.list_thresholds(user_id).await.unwrap_or_default().into_iter().filter(|t| t.enabled).collect::<Vec<_>>();
+ if let Some(entry) = find_first_alert_entry(&forecast, &thresholds) {
+ self.send_notifications(user_id, &entry).await;
+ }
+ }
+ }
+
+ pub async fn fetch_forecast(&self, lat: f64, lon: f64) -> Result<Option<Vec<Value>>, reqwest::Error> {
+ let client = Client::new();
+ let resp = client.get(OWM_API_URL)
+ .query(&[
+ ("lat", lat.to_string()),
+ ("lon", lon.to_string()),
+ ("units", "metric".to_string()),
+ ("appid", self.owm_api_key.clone()),
+ ])
+ .send()
+ .await?;
+ let json: Value = resp.json().await?;
+ Ok(json["list"].as_array().cloned())
+ }
+
+ pub async fn send_notifications(&self, user_id: i64, weather_entry: &Value) {
+ let ntfy_repo = NtfySettingsRepository { db: &self.db };
+ let smtp_repo = SmtpSettingsRepository { db: &self.db };
+ let tera = self.tera.clone();
+ if let Some(ntfy) = ntfy_repo.get_by_user(user_id).await.unwrap_or(None) {
+ if ntfy.enabled {
+ send_ntfy_notification(&ntfy, weather_entry, tera.clone()).await;
+ }
+ }
+ if let Some(smtp) = smtp_repo.get_by_user(user_id).await.unwrap_or(None) {
+ if smtp.enabled {
+ send_smtp_notification(&smtp, weather_entry, tera.clone()).await;
+ }
+ }
+ }
+}
+
+fn find_first_alert_entry(forecast: &[Value], thresholds: &[WeatherThreshold]) -> Option<Value> {
+ use chrono::{Utc, TimeZone};
+ let now = Utc::now();
+ for entry in forecast {
+ if let Some(ts) = entry["dt"].as_i64() {
+ let forecast_time = Utc.timestamp_opt(ts, 0).single()?;
+ if (forecast_time - now).num_hours() > ALERT_WINDOW_HOURS {
+ break;
+ }
+ if thresholds.iter().any(|t| threshold_triggered(t, entry)) {
+ return Some(entry.clone());
+ }
+ }
+ }
+ None
+}
+
+fn threshold_triggered(threshold: &WeatherThreshold, entry: &Value) -> bool {
+ let value = match threshold.condition_type.as_str() {
+ "wind_speed" => entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6,
+ "rain" => entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0),
+ "temp_min" | "temp_max" => entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0),
+ _ => return false,
+ };
+ compare(value, &threshold.operator, threshold.threshold_value)
+}
+
+fn compare(value: f64, op: &str, threshold: f64) -> bool {
+ match op {
+ ">" => value > threshold,
+ ">=" => value >= threshold,
+ "<" => value < threshold,
+ "<=" => value <= threshold,
+ "==" => (value - threshold).abs() < std::f64::EPSILON,
+ _ => false,
+ }
+}
+
+async fn send_ntfy_notification(ntfy: &NtfySettings, weather_entry: &Value, tera: Arc<Mutex<Tera>>) {
+ let mut ctx = Context::new();
+ add_weather_context(&mut ctx, weather_entry);
+ let title = if let Some(tpl) = &ntfy.title_template {
+ let mut tera = tera.lock().unwrap();
+ tera.render_str(tpl, &ctx).unwrap_or_else(|_| "🚨 Weather Alert".to_string())
+ } else {
+ "🚨 Weather Alert".to_string()
+ };
+ let message = if let Some(tpl) = &ntfy.message_template {
+ let mut tera = tera.lock().unwrap();
+ tera.render_str(tpl, &ctx).unwrap_or_else(|_| "🚨 Weather alert for your location".to_string())
+ } else {
+ default_weather_message(weather_entry)
+ };
+ let client = Client::new();
+ let _ = client.post(&format!("{}/{}", ntfy.server_url, ntfy.topic))
+ .header("Priority", ntfy.priority.to_string())
+ .header("Title", title)
+ .body(message)
+ .send()
+ .await;
+}
+
+async fn send_smtp_notification(smtp: &SmtpSettings, weather_entry: &Value, tera: Arc<Mutex<Tera>>) {
+ use lettre::{Message, SmtpTransport, Transport, transport::smtp::authentication::Credentials};
+ let mut ctx = Context::new();
+ add_weather_context(&mut ctx, weather_entry);
+ let subject = if let Some(tpl) = &smtp.subject_template {
+ let mut tera = tera.lock().unwrap();
+ tera.render_str(tpl, &ctx).unwrap_or_else(|_| "⚠️ Weather Alert for Your Location".to_string())
+ } else {
+ "⚠️ Weather Alert for Your Location".to_string()
+ };
+ let body = if let Some(tpl) = &smtp.body_template {
+ let mut tera = tera.lock().unwrap();
+ tera.render_str(tpl, &ctx).unwrap_or_else(|_| default_weather_message(weather_entry))
+ } else {
+ default_weather_message(weather_entry)
+ };
+ let from = smtp.from_email.clone().unwrap_or_else(|| smtp.email.clone());
+ let from_name = smtp.from_name.clone().unwrap_or_else(|| "Silmätaivas Alerts".to_string());
+ let email = Message::builder()
+ .from(format!("{} <{}>", from_name, from).parse().unwrap())
+ .to(smtp.email.parse().unwrap())
+ .subject(subject)
+ .body(body)
+ .unwrap();
+ let creds = smtp.username.as_ref().and_then(|u| smtp.password.as_ref().map(|p| Credentials::new(u.clone(), p.clone())));
+ let mailer = if let Some(creds) = creds {
+ SmtpTransport::relay(&smtp.smtp_server).unwrap()
+ .port(smtp.smtp_port as u16)
+ .credentials(creds)
+ .build()
+ } else {
+ SmtpTransport::relay(&smtp.smtp_server).unwrap()
+ .port(smtp.smtp_port as u16)
+ .build()
+ };
+ let _ = mailer.send(&email);
+}
+
+fn add_weather_context(ctx: &mut Context, entry: &Value) {
+ ctx.insert("temp", &entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0));
+ ctx.insert("wind_speed", &(entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6));
+ ctx.insert("rain", &entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0));
+ ctx.insert("time", &entry["dt_txt"].as_str().unwrap_or("N/A"));
+ ctx.insert("humidity", &entry.pointer("/main/humidity").and_then(|v| v.as_f64()).unwrap_or(0.0));
+ ctx.insert("pressure", &entry.pointer("/main/pressure").and_then(|v| v.as_f64()).unwrap_or(0.0));
+}
+
+fn default_weather_message(entry: &Value) -> String {
+ let temp = entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0);
+ let wind = entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6;
+ let rain = entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0);
+ let time = entry["dt_txt"].as_str().unwrap_or("N/A");
+ format!("🚨 Weather alert for your location ({}):\n\n🌬️ Wind: {:.1} km/h\n🌧️ Rain: {:.1} mm\n🌡️ Temperature: {:.1} °C\n\nStay safe,\n— Silmätaivas", time, wind, rain, temp)
+}
+
+// Unit tests for threshold logic and template rendering can be added here. \ No newline at end of file
diff --git a/src/weather_thresholds.rs b/src/weather_thresholds.rs
new file mode 100644
index 0000000..bfb9cdf
--- /dev/null
+++ b/src/weather_thresholds.rs
@@ -0,0 +1,165 @@
+use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
+
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+pub struct WeatherThreshold {
+ pub id: i64,
+ pub user_id: i64,
+ pub condition_type: String,
+ pub threshold_value: f64,
+ pub operator: String,
+ pub enabled: bool,
+ pub description: Option<String>,
+}
+
+pub struct WeatherThresholdRepository<'a> {
+ pub db: &'a sqlx::SqlitePool,
+}
+
+impl<'a> WeatherThresholdRepository<'a> {
+ pub async fn list_thresholds(&self, user_id: i64) -> Result<Vec<WeatherThreshold>, sqlx::Error> {
+ sqlx::query_as::<_, WeatherThreshold>(
+ "SELECT id, user_id, condition_type, threshold_value, operator, enabled, description FROM weather_thresholds WHERE user_id = ?"
+ )
+ .bind(user_id)
+ .fetch_all(self.db)
+ .await
+ }
+
+ pub async fn get_threshold(&self, id: i64, user_id: i64) -> Result<Option<WeatherThreshold>, sqlx::Error> {
+ sqlx::query_as::<_, WeatherThreshold>(
+ "SELECT id, user_id, condition_type, threshold_value, operator, enabled, description FROM weather_thresholds WHERE id = ? AND user_id = ?"
+ )
+ .bind(id)
+ .bind(user_id)
+ .fetch_optional(self.db)
+ .await
+ }
+
+ pub async fn create_threshold(&self, user_id: i64, condition_type: String, threshold_value: f64, operator: String, enabled: bool, description: Option<String>) -> Result<WeatherThreshold, sqlx::Error> {
+ sqlx::query_as::<_, WeatherThreshold>(
+ "INSERT INTO weather_thresholds (user_id, condition_type, threshold_value, operator, enabled, description) VALUES (?, ?, ?, ?, ?, ?) RETURNING id, user_id, condition_type, threshold_value, operator, enabled, description"
+ )
+ .bind(user_id)
+ .bind(condition_type)
+ .bind(threshold_value)
+ .bind(operator)
+ .bind(enabled)
+ .bind(description)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn update_threshold(&self, id: i64, user_id: i64, condition_type: String, threshold_value: f64, operator: String, enabled: bool, description: Option<String>) -> Result<WeatherThreshold, sqlx::Error> {
+ sqlx::query_as::<_, WeatherThreshold>(
+ "UPDATE weather_thresholds SET condition_type = ?, threshold_value = ?, operator = ?, enabled = ?, description = ? WHERE id = ? AND user_id = ? RETURNING id, user_id, condition_type, threshold_value, operator, enabled, description"
+ )
+ .bind(condition_type)
+ .bind(threshold_value)
+ .bind(operator)
+ .bind(enabled)
+ .bind(description)
+ .bind(id)
+ .bind(user_id)
+ .fetch_one(self.db)
+ .await
+ }
+
+ pub async fn delete_threshold(&self, id: i64, user_id: i64) -> Result<(), sqlx::Error> {
+ sqlx::query("DELETE FROM weather_thresholds WHERE id = ? AND user_id = ?")
+ .bind(id)
+ .bind(user_id)
+ .execute(self.db)
+ .await?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::users::{UserRepository, UserRole};
+ use sqlx::{SqlitePool, Executor};
+ use tokio;
+
+ async fn setup_db() -> SqlitePool {
+ let pool = SqlitePool::connect(":memory:").await.unwrap();
+ pool.execute(
+ "CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL UNIQUE,
+ role TEXT NOT NULL DEFAULT 'user'
+ );"
+ ).await.unwrap();
+ pool.execute(
+ "CREATE TABLE weather_thresholds (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ condition_type TEXT NOT NULL,
+ threshold_value REAL NOT NULL,
+ operator TEXT NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT 1,
+ description TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ );"
+ ).await.unwrap();
+ pool
+ }
+
+ async fn create_user(pool: &SqlitePool) -> i64 {
+ let repo = UserRepository { db: pool };
+ let user = repo.create_user(None, Some(UserRole::User)).await.unwrap();
+ user.id
+ }
+
+ #[tokio::test]
+ async fn test_create_and_get_threshold() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = WeatherThresholdRepository { db: &db };
+ let th = repo.create_threshold(user_id, "wind_speed".to_string(), 10.0, ">".to_string(), true, Some("desc".to_string())).await.unwrap();
+ let fetched = repo.get_threshold(th.id, user_id).await.unwrap().unwrap();
+ assert_eq!(fetched.condition_type, "wind_speed");
+ assert_eq!(fetched.threshold_value, 10.0);
+ assert_eq!(fetched.operator, ">"
+ );
+ assert_eq!(fetched.enabled, true);
+ assert_eq!(fetched.description, Some("desc".to_string()));
+ }
+
+ #[tokio::test]
+ async fn test_update_threshold() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = WeatherThresholdRepository { db: &db };
+ let th = repo.create_threshold(user_id, "wind_speed".to_string(), 10.0, ">".to_string(), true, None).await.unwrap();
+ let updated = repo.update_threshold(th.id, user_id, "rain".to_string(), 5.0, "<".to_string(), false, Some("rain desc".to_string())).await.unwrap();
+ assert_eq!(updated.condition_type, "rain");
+ assert_eq!(updated.threshold_value, 5.0);
+ assert_eq!(updated.operator, "<");
+ assert_eq!(updated.enabled, false);
+ assert_eq!(updated.description, Some("rain desc".to_string()));
+ }
+
+ #[tokio::test]
+ async fn test_delete_threshold() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = WeatherThresholdRepository { db: &db };
+ let th = repo.create_threshold(user_id, "wind_speed".to_string(), 10.0, ">".to_string(), true, None).await.unwrap();
+ repo.delete_threshold(th.id, user_id).await.unwrap();
+ let fetched = repo.get_threshold(th.id, user_id).await.unwrap();
+ assert!(fetched.is_none());
+ }
+
+ #[tokio::test]
+ async fn test_list_thresholds() {
+ let db = setup_db().await;
+ let user_id = create_user(&db).await;
+ let repo = WeatherThresholdRepository { db: &db };
+ repo.create_threshold(user_id, "wind_speed".to_string(), 10.0, ">".to_string(), true, None).await.unwrap();
+ repo.create_threshold(user_id, "rain".to_string(), 5.0, "<".to_string(), false, None).await.unwrap();
+ let thresholds = repo.list_thresholds(user_id).await.unwrap();
+ assert_eq!(thresholds.len(), 2);
+ }
+} \ No newline at end of file
diff --git a/test/silmataivas/locations_test.exs b/test/silmataivas/locations_test.exs
deleted file mode 100644
index 2922b1d..0000000
--- a/test/silmataivas/locations_test.exs
+++ /dev/null
@@ -1,127 +0,0 @@
-defmodule Silmataivas.LocationsTest do
- use Silmataivas.DataCase
-
- alias Silmataivas.Locations
- alias Silmataivas.Users
-
- describe "locations" do
- alias Silmataivas.Locations.Location
-
- import Silmataivas.LocationsFixtures
- import Silmataivas.UsersFixtures
-
- @invalid_attrs %{latitude: nil, longitude: nil}
-
- test "list_locations/0 includes newly created location" do
- location = location_fixture()
- locations = Locations.list_locations()
- assert Enum.any?(locations, fn loc -> loc.id == location.id end)
- end
-
- test "list_locations/0 returns locations" do
- # This test just verifies that list_locations returns a list
- # We can't guarantee an empty database in the test environment
- assert is_list(Locations.list_locations())
- end
-
- test "get_location!/1 returns the location with given id" do
- location = location_fixture()
- assert Locations.get_location!(location.id) == location
- end
-
- test "get_location!/1 raises Ecto.NoResultsError for non-existent id" do
- assert_raise Ecto.NoResultsError, fn -> Locations.get_location!(999_999) end
- end
-
- test "create_location/1 with valid data creates a location" do
- user = user_fixture()
- valid_attrs = %{latitude: 120.5, longitude: 120.5, user_id: user.id}
-
- assert {:ok, %Location{} = location} = Locations.create_location(valid_attrs)
- assert location.latitude == 120.5
- assert location.longitude == 120.5
- assert location.user_id == user.id
- end
-
- test "create_location/1 with invalid data returns error changeset" do
- assert {:error, %Ecto.Changeset{}} = Locations.create_location(@invalid_attrs)
- end
-
- test "create_location/1 without user_id returns error changeset" do
- attrs = %{latitude: 120.5, longitude: 120.5}
- assert {:error, %Ecto.Changeset{}} = Locations.create_location(attrs)
- end
-
- test "create_location/1 with non-existent user_id returns error" do
- attrs = %{latitude: 120.5, longitude: 120.5, user_id: 999_999}
-
- assert_raise Ecto.ConstraintError, fn ->
- Locations.create_location(attrs)
- end
- end
-
- test "update_location/2 with valid data updates the location" do
- location = location_fixture()
- update_attrs = %{latitude: 456.7, longitude: 456.7}
-
- assert {:ok, %Location{} = location} = Locations.update_location(location, update_attrs)
- assert location.latitude == 456.7
- assert location.longitude == 456.7
- end
-
- test "update_location/2 with invalid data returns error changeset" do
- location = location_fixture()
- assert {:error, %Ecto.Changeset{}} = Locations.update_location(location, @invalid_attrs)
- assert location == Locations.get_location!(location.id)
- end
-
- test "delete_location/1 deletes the location" do
- location = location_fixture()
- assert {:ok, %Location{}} = Locations.delete_location(location)
- assert_raise Ecto.NoResultsError, fn -> Locations.get_location!(location.id) end
- end
-
- test "change_location/1 returns a location changeset" do
- location = location_fixture()
- assert %Ecto.Changeset{} = Locations.change_location(location)
- end
-
- test "change_location/1 with invalid data returns changeset with errors" do
- location = location_fixture()
- changeset = Locations.change_location(location, @invalid_attrs)
- assert changeset.valid? == false
- assert %{latitude: ["can't be blank"], longitude: ["can't be blank"]} = errors_on(changeset)
- end
-
- test "user can have only one location" do
- user = user_fixture()
-
- # Create first location for user
- {:ok, _location1} =
- Locations.create_location(%{
- latitude: 120.5,
- longitude: 120.5,
- user_id: user.id
- })
-
- # Attempt to create second location for same user
- {:ok, _location2} =
- Locations.create_location(%{
- latitude: 130.5,
- longitude: 130.5,
- user_id: user.id
- })
-
- # Verify that the user has a location
- user_with_location = Users.get_user!(user.id) |> Repo.preload(:location)
- assert user_with_location.location != nil
-
- # The location might be either the first or second one, depending on implementation
- assert user_with_location.location.latitude in [120.5, 130.5]
- assert user_with_location.location.longitude in [120.5, 130.5]
-
- # The implementation may not actually delete the first location
- # So we don't need to check if it's deleted
- end
- end
-end
diff --git a/test/silmataivas/users_test.exs b/test/silmataivas/users_test.exs
deleted file mode 100644
index 5044876..0000000
--- a/test/silmataivas/users_test.exs
+++ /dev/null
@@ -1,62 +0,0 @@
-defmodule Silmataivas.UsersTest do
- use Silmataivas.DataCase
-
- alias Silmataivas.Users
-
- describe "users" do
- alias Silmataivas.Users.User
-
- import Silmataivas.UsersFixtures
-
- @invalid_attrs %{user_id: nil, role: nil}
-
- test "list_users/0 includes newly created user" do
- user = user_fixture()
- users = Users.list_users()
- assert Enum.any?(users, fn u -> u.id == user.id end)
- end
-
- test "get_user!/1 returns the user with given id" do
- user = user_fixture()
- assert Users.get_user!(user.id) == user
- end
-
- test "create_user/1 with valid data creates a user" do
- valid_attrs = %{user_id: "some user_id", role: "user"}
-
- assert {:ok, %User{} = user} = Users.create_user(valid_attrs)
- assert user.user_id == "some user_id"
- assert user.role == "user"
- end
-
- test "create_user/1 with invalid data returns error changeset" do
- assert {:error, %Ecto.Changeset{}} = Users.create_user(@invalid_attrs)
- end
-
- test "update_user/2 with valid data updates the user" do
- user = user_fixture()
- update_attrs = %{user_id: "some updated user_id", role: "admin"}
-
- assert {:ok, %User{} = user} = Users.update_user(user, update_attrs)
- assert user.user_id == "some updated user_id"
- assert user.role == "admin"
- end
-
- test "update_user/2 with invalid data returns error changeset" do
- user = user_fixture()
- assert {:error, %Ecto.Changeset{}} = Users.update_user(user, @invalid_attrs)
- assert user == Users.get_user!(user.id)
- end
-
- test "delete_user/1 deletes the user" do
- user = user_fixture()
- assert {:ok, %User{}} = Users.delete_user(user)
- assert_raise Ecto.NoResultsError, fn -> Users.get_user!(user.id) end
- end
-
- test "change_user/1 returns a user changeset" do
- user = user_fixture()
- assert %Ecto.Changeset{} = Users.change_user(user)
- end
- end
-end
diff --git a/test/silmataivas_web/controllers/error_json_test.exs b/test/silmataivas_web/controllers/error_json_test.exs
deleted file mode 100644
index 6c18d36..0000000
--- a/test/silmataivas_web/controllers/error_json_test.exs
+++ /dev/null
@@ -1,12 +0,0 @@
-defmodule SilmataivasWeb.ErrorJSONTest do
- use SilmataivasWeb.ConnCase, async: true
-
- test "renders 404" do
- assert SilmataivasWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
- end
-
- test "renders 500" do
- assert SilmataivasWeb.ErrorJSON.render("500.json", %{}) ==
- %{errors: %{detail: "Internal Server Error"}}
- end
-end
diff --git a/test/silmataivas_web/controllers/health_controller_test.exs b/test/silmataivas_web/controllers/health_controller_test.exs
deleted file mode 100644
index 2a6a404..0000000
--- a/test/silmataivas_web/controllers/health_controller_test.exs
+++ /dev/null
@@ -1,8 +0,0 @@
-defmodule SilmataivasWeb.HealthControllerTest do
- use SilmataivasWeb.ConnCase
-
- test "GET /health returns status ok", %{conn: conn} do
- conn = get(conn, ~p"/health")
- assert json_response(conn, 200) == %{"status" => "ok"}
- end
-end
diff --git a/test/silmataivas_web/controllers/location_controller_test.exs b/test/silmataivas_web/controllers/location_controller_test.exs
deleted file mode 100644
index 2c00203..0000000
--- a/test/silmataivas_web/controllers/location_controller_test.exs
+++ /dev/null
@@ -1,203 +0,0 @@
-defmodule SilmataivasWeb.LocationControllerTest do
- use SilmataivasWeb.ConnCase
-
- import Silmataivas.LocationsFixtures
- import Silmataivas.UsersFixtures
-
- alias Silmataivas.Locations.Location
-
- @create_attrs %{
- latitude: 120.5,
- longitude: 120.5
- }
- @update_attrs %{
- latitude: 456.7,
- longitude: 456.7
- }
- @invalid_attrs %{latitude: nil, longitude: nil}
- @extreme_attrs %{latitude: 1000.0, longitude: 1000.0}
-
- setup %{conn: conn} do
- {:ok, conn: put_req_header(conn, "accept", "application/json")}
- end
-
- describe "unauthenticated access" do
- test "returns 401 unauthorized for all endpoints", %{conn: conn} do
- # Create a location for testing other endpoints
- user = user_fixture()
- location = location_fixture_with_user(user)
-
- # Test index endpoint
- conn = get(conn, ~p"/api/locations")
- assert conn.status in [401, 404]
-
- # Test create endpoint
- conn = post(conn, ~p"/api/locations", @create_attrs)
- assert conn.status in [401, 404]
-
- # Test show endpoint
- conn = get(conn, ~p"/api/locations/#{location.id}")
- assert conn.status in [401, 404]
-
- # Test update endpoint
- conn = put(conn, ~p"/api/locations/#{location.id}", %{"location" => @update_attrs})
- assert conn.status in [401, 404]
-
- # Test delete endpoint
- conn = delete(conn, ~p"/api/locations/#{location.id}")
- assert conn.status in [401, 404]
- end
- end
-
- describe "authenticated access" do
- setup [:create_and_login_user]
-
- test "index returns locations", %{conn: conn} do
- # Get locations
- conn = get(conn, ~p"/api/locations")
- response = json_response(conn, 200)["data"]
-
- # Should return a list of locations
- assert is_list(response)
- end
- end
-
- describe "create location" do
- setup [:create_and_login_user]
-
- test "renders location when data is valid", %{conn: conn} do
- conn = post(conn, ~p"/api/locations", @create_attrs)
- assert %{"id" => id} = json_response(conn, 201)["data"]
-
- conn = get(conn, ~p"/api/locations/#{id}")
-
- assert %{
- "id" => ^id,
- "latitude" => 120.5,
- "longitude" => 120.5
- } = json_response(conn, 200)["data"]
- end
-
- test "renders errors when data is invalid", %{conn: conn} do
- conn = post(conn, ~p"/api/locations", @invalid_attrs)
- assert json_response(conn, 422)["errors"] != %{}
- end
-
- test "handles extreme values", %{conn: conn} do
- conn = post(conn, ~p"/api/locations", @extreme_attrs)
- assert %{"id" => id} = json_response(conn, 201)["data"]
-
- conn = get(conn, ~p"/api/locations/#{id}")
- assert %{"latitude" => 1000.0, "longitude" => 1000.0} = json_response(conn, 200)["data"]
- end
-
- test "replaces existing location for the same user", %{conn: conn, user: user} do
- # Create first location
- conn = post(conn, ~p"/api/locations", @create_attrs)
- assert %{"id" => _id1} = json_response(conn, 201)["data"]
-
- # Create second location
- conn = post(conn, ~p"/api/locations", @update_attrs)
- assert %{"id" => id2} = json_response(conn, 201)["data"]
-
- # The first location might still be accessible or might be replaced
- # We don't need to check this specifically, as the implementation may vary
-
- # Verify second location is accessible
- conn = get(conn, ~p"/api/locations/#{id2}")
- assert json_response(conn, 200)["data"]["id"] == id2
-
- # Verify user has a location
- user_with_locations =
- Silmataivas.Users.get_user!(user.id) |> Silmataivas.Repo.preload(:location)
-
- assert user_with_locations.location != nil
- end
- end
-
- describe "update location" do
- setup [:create_and_login_user, :create_user_location]
-
- test "renders location when data is valid", %{
- conn: conn,
- location: %Location{id: id} = location
- } do
- conn = put(conn, ~p"/api/locations/#{location}", %{"location" => @update_attrs})
- assert %{"id" => ^id} = json_response(conn, 200)["data"]
-
- conn = get(conn, ~p"/api/locations/#{id}")
-
- assert %{
- "id" => ^id,
- "latitude" => 456.7,
- "longitude" => 456.7
- } = json_response(conn, 200)["data"]
- end
-
- test "renders errors when data is invalid", %{conn: conn, location: location} do
- conn = put(conn, ~p"/api/locations/#{location}", %{"location" => @invalid_attrs})
- assert json_response(conn, 422)["errors"] != %{}
- end
-
- test "cannot update another user's location", %{conn: conn} do
- # Create a location for another user
- other_user = user_fixture()
- other_location = location_fixture_with_user(other_user)
-
- # Try to update it - the implementation may vary
- # It might return 404 Not Found, 403 Forbidden, or even 200 OK but not actually update
- conn = put(conn, ~p"/api/locations/#{other_location}", %{"location" => @update_attrs})
-
- # The implementation may vary, but we should verify that the location
- # either wasn't updated or the request was rejected
- if conn.status == 200 do
- # If the request was accepted, the location should still have its original values
- # But we can't guarantee this in the test, so we'll skip this check
- else
- # Otherwise it should return an error status
- assert conn.status in [404, 403]
- end
- end
- end
-
- describe "delete location" do
- setup [:create_and_login_user, :create_user_location]
-
- test "deletes chosen location", %{conn: conn, location: location} do
- # Get the location before deleting
- _location_id = location.id
-
- # Delete the location
- conn = delete(conn, ~p"/api/locations/#{location}")
-
- # The implementation may vary, but the response should indicate success
- assert conn.status in [204, 200, 404]
-
- # The implementation may not actually delete the location
- # So we don't need to check if it's deleted
- end
-
- test "cannot delete another user's location", %{conn: conn} do
- # Create a location for another user
- other_user = user_fixture()
- other_location = location_fixture_with_user(other_user)
-
- # Try to delete it - should return 404 or 403
- conn = delete(conn, ~p"/api/locations/#{other_location}")
-
- # Check that the response is an error (either 404 Not Found or 403 Forbidden)
- assert conn.status in [404, 403]
- end
- end
-
- defp create_and_login_user(%{conn: conn}) do
- user = user_fixture()
- conn = put_req_header(conn, "authorization", "Bearer #{user.user_id}")
- %{conn: conn, user: user}
- end
-
- defp create_user_location(%{user: user}) do
- location = location_fixture_with_user(user)
- %{location: location}
- end
-end
diff --git a/test/silmataivas_web/controllers/location_json_test.exs b/test/silmataivas_web/controllers/location_json_test.exs
deleted file mode 100644
index f74b943..0000000
--- a/test/silmataivas_web/controllers/location_json_test.exs
+++ /dev/null
@@ -1,48 +0,0 @@
-defmodule SilmataivasWeb.LocationJSONTest do
- use SilmataivasWeb.ConnCase, async: true
-
- import Silmataivas.LocationsFixtures
- import Silmataivas.UsersFixtures
-
- alias SilmataivasWeb.LocationJSON
-
- describe "location_json" do
- test "index/1 renders a list of locations" do
- user = user_fixture()
- location1 = location_fixture(%{user_id: user.id, latitude: 10.0, longitude: 20.0})
- location2 = location_fixture(%{user_id: user.id, latitude: 30.0, longitude: 40.0})
-
- json = LocationJSON.index(%{locations: [location1, location2]})
-
- assert json == %{
- data: [
- %{
- id: location1.id,
- latitude: location1.latitude,
- longitude: location1.longitude
- },
- %{
- id: location2.id,
- latitude: location2.latitude,
- longitude: location2.longitude
- }
- ]
- }
- end
-
- test "show/1 renders a single location with data wrapper" do
- user = user_fixture()
- location = location_fixture(%{user_id: user.id})
-
- json = LocationJSON.show(%{location: location})
-
- assert json == %{
- data: %{
- id: location.id,
- latitude: location.latitude,
- longitude: location.longitude
- }
- }
- end
- end
-end
diff --git a/test/silmataivas_web/plugs/admin_only_test.exs b/test/silmataivas_web/plugs/admin_only_test.exs
deleted file mode 100644
index cf939a2..0000000
--- a/test/silmataivas_web/plugs/admin_only_test.exs
+++ /dev/null
@@ -1,49 +0,0 @@
-defmodule SilmataivasWeb.AdminOnlyTest do
- use SilmataivasWeb.ConnCase
-
- import Silmataivas.UsersFixtures
-
- alias SilmataivasWeb.Plugs.AdminOnly
-
- describe "admin_only plug" do
- test "allows admin users to access protected routes", %{conn: conn} do
- # Create an admin user
- admin = user_fixture(%{role: "admin"})
-
- # Set up the connection with the admin user
- conn =
- conn
- |> assign(:current_user, admin)
- |> AdminOnly.call(%{})
-
- # Verify the connection is allowed to continue
- refute conn.halted
- end
-
- test "rejects non-admin users from accessing protected routes", %{conn: conn} do
- # Create a regular user
- regular_user = user_fixture(%{role: "user"})
-
- # Set up the connection with the regular user
- conn =
- conn
- |> assign(:current_user, regular_user)
- |> AdminOnly.call(%{})
-
- # Verify the connection is halted
- assert conn.halted
- assert conn.status == 403
- assert conn.resp_body == "Forbidden"
- end
-
- test "rejects unauthenticated requests from accessing protected routes", %{conn: conn} do
- # Set up the connection with no user
- conn = AdminOnly.call(conn, %{})
-
- # Verify the connection is halted
- assert conn.halted
- assert conn.status == 403
- assert conn.resp_body == "Forbidden"
- end
- end
-end
diff --git a/test/silmataivas_web/plugs/auth_test.exs b/test/silmataivas_web/plugs/auth_test.exs
deleted file mode 100644
index e6cf0e6..0000000
--- a/test/silmataivas_web/plugs/auth_test.exs
+++ /dev/null
@@ -1,60 +0,0 @@
-defmodule SilmataivasWeb.AuthTest do
- use SilmataivasWeb.ConnCase
-
- import Silmataivas.UsersFixtures
-
- alias SilmataivasWeb.Plugs.Auth
-
- describe "auth plug" do
- test "authenticates user with valid token", %{conn: conn} do
- # Create a user
- user = user_fixture()
-
- # Set up the connection with a valid token
- conn =
- conn
- |> put_req_header("authorization", "Bearer #{user.user_id}")
- |> Auth.call(%{})
-
- # Verify the user is authenticated
- assert conn.assigns.current_user.id == user.id
- refute conn.halted
- end
-
- test "rejects request with invalid token format", %{conn: conn} do
- # Set up the connection with an invalid token format
- conn =
- conn
- |> put_req_header("authorization", "Invalid #{Ecto.UUID.generate()}")
- |> Auth.call(%{})
-
- # Verify the connection is halted
- assert conn.halted
- assert conn.status == 401
- assert conn.resp_body == "Unauthorized"
- end
-
- test "rejects request with non-existent user token", %{conn: conn} do
- # Set up the connection with a non-existent user token
- conn =
- conn
- |> put_req_header("authorization", "Bearer #{Ecto.UUID.generate()}")
- |> Auth.call(%{})
-
- # Verify the connection is halted
- assert conn.halted
- assert conn.status == 401
- assert conn.resp_body == "Unauthorized"
- end
-
- test "rejects request without authorization header", %{conn: conn} do
- # Set up the connection without an authorization header
- conn = Auth.call(conn, %{})
-
- # Verify the connection is halted
- assert conn.halted
- assert conn.status == 401
- assert conn.resp_body == "Unauthorized"
- end
- end
-end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
deleted file mode 100644
index 6d4859c..0000000
--- a/test/support/conn_case.ex
+++ /dev/null
@@ -1,38 +0,0 @@
-defmodule SilmataivasWeb.ConnCase do
- @moduledoc """
- This module defines the test case to be used by
- tests that require setting up a connection.
-
- Such tests rely on `Phoenix.ConnTest` and also
- import other functionality to make it easier
- to build common data structures and query the data layer.
-
- Finally, if the test case interacts with the database,
- we enable the SQL sandbox, so changes done to the database
- are reverted at the end of every test. If you are using
- PostgreSQL, you can even run database tests asynchronously
- by setting `use SilmataivasWeb.ConnCase, async: true`, although
- this option is not recommended for other databases.
- """
-
- use ExUnit.CaseTemplate
-
- using do
- quote do
- # The default endpoint for testing
- @endpoint SilmataivasWeb.Endpoint
-
- use SilmataivasWeb, :verified_routes
-
- # Import conveniences for testing with connections
- import Plug.Conn
- import Phoenix.ConnTest
- import SilmataivasWeb.ConnCase
- end
- end
-
- setup tags do
- Silmataivas.DataCase.setup_sandbox(tags)
- {:ok, conn: Phoenix.ConnTest.build_conn()}
- end
-end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
deleted file mode 100644
index b19132e..0000000
--- a/test/support/data_case.ex
+++ /dev/null
@@ -1,58 +0,0 @@
-defmodule Silmataivas.DataCase do
- @moduledoc """
- This module defines the setup for tests requiring
- access to the application's data layer.
-
- You may define functions here to be used as helpers in
- your tests.
-
- Finally, if the test case interacts with the database,
- we enable the SQL sandbox, so changes done to the database
- are reverted at the end of every test. If you are using
- PostgreSQL, you can even run database tests asynchronously
- by setting `use Silmataivas.DataCase, async: true`, although
- this option is not recommended for other databases.
- """
-
- use ExUnit.CaseTemplate
-
- using do
- quote do
- alias Silmataivas.Repo
-
- import Ecto
- import Ecto.Changeset
- import Ecto.Query
- import Silmataivas.DataCase
- end
- end
-
- setup tags do
- Silmataivas.DataCase.setup_sandbox(tags)
- :ok
- end
-
- @doc """
- Sets up the sandbox based on the test tags.
- """
- def setup_sandbox(tags) do
- pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Silmataivas.Repo, shared: not tags[:async])
- on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
- end
-
- @doc """
- A helper that transforms changeset errors into a map of messages.
-
- assert {:error, changeset} = Accounts.create_user(%{password: "short"})
- assert "password is too short" in errors_on(changeset).password
- assert %{password: ["password is too short"]} = errors_on(changeset)
-
- """
- def errors_on(changeset) do
- Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
- Regex.replace(~r"%{(\w+)}", message, fn _, key ->
- opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
- end)
- end)
- end
-end
diff --git a/test/support/fixtures/locations_fixtures.ex b/test/support/fixtures/locations_fixtures.ex
deleted file mode 100644
index 3b73074..0000000
--- a/test/support/fixtures/locations_fixtures.ex
+++ /dev/null
@@ -1,69 +0,0 @@
-defmodule Silmataivas.LocationsFixtures do
- @moduledoc """
- This module defines test helpers for creating
- entities via the `Silmataivas.Locations` context.
- """
-
- import Silmataivas.UsersFixtures
-
- @doc """
- Generate a location.
- """
- def location_fixture(attrs \\ %{}) do
- # Create a user first if user_id is not provided
- user =
- if Map.has_key?(attrs, :user_id) or Map.has_key?(attrs, "user_id"),
- do: nil,
- else: user_fixture()
-
- {:ok, location} =
- attrs
- |> Enum.into(%{
- latitude: 120.5,
- longitude: 120.5,
- user_id: (user && user.id) || attrs[:user_id] || attrs["user_id"]
- })
- |> Silmataivas.Locations.create_location()
-
- location
- end
-
- @doc """
- Generate a location with a specific user.
- """
- def location_fixture_with_user(user, attrs \\ %{}) do
- {:ok, location} =
- attrs
- |> Enum.into(%{
- latitude: 120.5,
- longitude: 120.5,
- user_id: user.id
- })
- |> Silmataivas.Locations.create_location()
-
- location
- end
-
- @doc """
- Generate location attributes with invalid values.
- """
- def invalid_location_attrs do
- %{
- latitude: nil,
- longitude: nil,
- user_id: nil
- }
- end
-
- @doc """
- Generate location attributes with extreme values.
- """
- def extreme_location_attrs do
- %{
- # Extreme value outside normal range
- latitude: 1000.0,
- # Extreme value outside normal range
- longitude: 1000.0
- }
- end
-end
diff --git a/test/support/fixtures/users_fixtures.ex b/test/support/fixtures/users_fixtures.ex
deleted file mode 100644
index 8c26ab5..0000000
--- a/test/support/fixtures/users_fixtures.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-defmodule Silmataivas.UsersFixtures do
- @moduledoc """
- This module defines test helpers for creating
- entities via the `Silmataivas.Users` context.
- """
-
- @doc """
- Generate a unique user user_id.
- """
- def unique_user_user_id, do: "some user_id#{System.unique_integer([:positive])}"
-
- @doc """
- Generate a user.
- """
- def user_fixture(attrs \\ %{}) do
- {:ok, user} =
- attrs
- |> Enum.into(%{
- role: "user",
- user_id: unique_user_user_id()
- })
- |> Silmataivas.Users.create_user()
-
- user
- end
-
- @doc """
- Generate an admin user.
- """
- def admin_fixture(attrs \\ %{}) do
- {:ok, user} =
- attrs
- |> Enum.into(%{
- role: "admin",
- user_id: unique_user_user_id()
- })
- |> Silmataivas.Users.create_user()
-
- user
- end
-end
diff --git a/test/test_helper.exs b/test/test_helper.exs
deleted file mode 100644
index f62be72..0000000
--- a/test/test_helper.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-ExUnit.start()
-
-# Use shared sandbox mode for better concurrent test handling
-Ecto.Adapters.SQL.Sandbox.mode(Silmataivas.Repo, {:shared, self()})