diff --git a/.env b/.env index f33b818..1ddddec 100644 --- a/.env +++ b/.env @@ -38,7 +38,13 @@ SSL_CERT_KEY_PATH=../ssl/localhost.key SMTP_USERNAME=smtp-user SMTP_PASSWORD=smtp-password SMTP_FROM=noreply@example.com -SMTP_SERVER=smtp.example.com +SMTP_SERVER=localhost +SMTP_PORT=1025 +#SMTP_USERNAME=smtp-user +#SMTP_PASSWORD=smtp-password +#SMTP_FROM=noreply@example.com +#SMTP_SERVER=smtp.example.com +#SMTP_PORT=587 VITE_API_URL=${EXTERNAL_URL}/api VITE_DEFAULT_LIMIT=200 @@ -46,9 +52,13 @@ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=${NGINX_HOST} ENVIRONMENT=development +ADMIN_USERNAME=admin ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=changeme TEMPLATE_DIR=../templates -AVIATION_WEATHER_URL=https://aviationweather.gov/api/data +MAILPIT_WEB_PORT=8025 +MAILPIT_SMTP_PORT=1025 + +AVIATION_WEATHER_URL=https://aviationweather.gov diff --git a/Makefile b/Makefile index 78f4306..f752ac2 100644 --- a/Makefile +++ b/Makefile @@ -76,24 +76,24 @@ down-backend: backend-down run: ## Run the api @cd api && cargo run -frontend-up: ## Start Docker containers - @docker compose --profile frontend up -d +dev-up: ## Start Docker containers + @docker compose --profile dev up -d -up-frontend: frontend-up +up-dev: dev-up -frontend-down: ## Stop Docker containers - @docker compose --profile frontend down +dev-down: ## Stop Docker containers + @docker compose --profile dev down -down-frontend: frontend-down +down-dev: dev-down docker-prune: ## Prune the docker system @docker system prune -a docker-clean: ## Stop the docker containers and remove volumes - @docker compose --profile frontend --profile api --profile backend down -v + @docker compose --profile dev --profile api --profile backend down -v docker-down: ## Stop the docker container - @docker compose --profile frontend --profile api --profile backend down + @docker compose --profile dev --profile api --profile backend down docker-up: ## Start the docker container @docker compose --profile backend --profile api up -d @@ -122,6 +122,10 @@ push: registry=$(if $(r),$(r),gitea.bensherriff.com/bsherriff) push: platform=$(if $(p),$(p),linux/amd64,linux/arm64) push: image=${registry}/aviation-${folder}:${version} push: ## Build and push a specific docker image (`make push f=httpd`) + docker buildx create \ + --use \ + --name aviation-builder \ + --platform ${platform} || true; \ docker buildx build \ -f ${folder}/Dockerfile \ --platform ${platform} \ diff --git a/api/Cargo.lock b/api/Cargo.lock deleted file mode 100644 index 16a3f65..0000000 --- a/api/Cargo.lock +++ /dev/null @@ -1,4361 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more 2.0.1", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more 2.0.1", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2 0.3.26", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.1", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-multipart" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" -dependencies = [ - "actix-multipart-derive", - "actix-utils", - "actix-web", - "derive_more 0.99.19", - "futures-core", - "futures-util", - "httparse", - "local-waker", - "log", - "memchr", - "mime", - "rand 0.8.5", - "serde", - "serde_json", - "serde_plain", - "tempfile", - "tokio", -] - -[[package]] -name = "actix-multipart-derive" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" -dependencies = [ - "darling", - "parse-size", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more 2.0.1", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy 0.8.24", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "api" -version = "0.1.1" -dependencies = [ - "actix-cors", - "actix-multipart", - "actix-web", - "argon2", - "chrono", - "dotenv", - "env_logger", - "futures", - "futures-util", - "handlebars", - "lettre", - "log", - "rand 0.9.1", - "rand_chacha 0.9.0", - "redis", - "regex", - "reqwest", - "rust-s3", - "serde", - "serde_json", - "sqlx", - "tokio", - "utoipa", - "utoipa-actix-web", - "utoipa-swagger-ui", - "uuid", - "webpki-roots", -] - -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - -[[package]] -name = "async-trait" -version = "0.1.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" -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 = "attohttpc" -version = "0.28.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07a9b245ba0739fc90935094c29adbaee3f977218b5fb95e822e261cda7f56a3" -dependencies = [ - "http 1.2.0", - "log", - "native-tls", - "serde", - "serde_json", - "url", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "aws-creds" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f84143206b9c72b3c5cb65415de60c7539c79cd1559290fddec657939131be0" -dependencies = [ - "attohttpc", - "home", - "log", - "quick-xml", - "rust-ini", - "serde", - "thiserror 1.0.69", - "time", - "url", -] - -[[package]] -name = "aws-region" -version = "0.25.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aed3f9c7eac9be28662fdb3b0f4d1951e812f7c64fed4f0327ba702f459b3b" -dependencies = [ - "thiserror 1.0.69", -] - -[[package]] -name = "backon" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496" -dependencies = [ - "fastrand", -] - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -dependencies = [ - "serde", -] - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - -[[package]] -name = "cc" -version = "1.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "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 = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[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 = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.15", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -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 = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[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 = "crunchy" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" - -[[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 = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] - -[[package]] -name = "derive_more" -version = "0.99.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[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 = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - -[[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 = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[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 = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "flate2" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" - -[[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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.2.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "handlebars" -version = "6.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" -dependencies = [ - "derive_builder", - "log", - "num-order", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror 2.0.12", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.2", -] - -[[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 = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.2.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "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 = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.8", - "http 1.2.0", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http 1.2.0", - "hyper 1.6.0", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "hyper 1.6.0", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "indexmap" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", - "serde", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "lettre" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ffd14fa289730e3ad68edefdc31f603d56fe716ec38f2076bb7410e09147c2" -dependencies = [ - "async-trait", - "base64", - "chumsky", - "email-encoding", - "email_address", - "fastrand", - "futures-io", - "futures-util", - "hostname", - "httpdate", - "idna", - "mime", - "native-tls", - "nom", - "percent-encoding", - "quoted_printable", - "socket2", - "tokio", - "tokio-native-tls", - "url", -] - -[[package]] -name = "libc" -version = "0.2.170" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" - -[[package]] -name = "libm" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" - -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "maybe-async" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minidom" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f45614075738ce1b77a1768912a60c0227525971b03e09122a05b8a34a2a6278" -dependencies = [ - "rxml", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "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 = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[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 0.8.5", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-modular" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" - -[[package]] -name = "num-order" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" -dependencies = [ - "num-modular", -] - -[[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.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" - -[[package]] -name = "openssl" -version = "0.10.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" -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.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "parse-size" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" -dependencies = [ - "memchr", - "thiserror 2.0.12", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] -name = "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 = "portable-atomic" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy 0.7.35", -] - -[[package]] -name = "proc-macro2" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" -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 = "quick-xml" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quote" -version = "1.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" -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 = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[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 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.1", -] - -[[package]] -name = "redis" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" -dependencies = [ - "arc-swap", - "backon", - "bytes", - "cfg-if", - "combine", - "futures-channel", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "r2d2", - "ryu", - "serde", - "serde_json", - "sha1_smol", - "socket2", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redox_syscall" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" -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-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.4.8", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-rustls", - "hyper-tls 0.6.0", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-registry", -] - -[[package]] -name = "ring" -version = "0.17.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rsa" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rust-embed" -version = "8.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e425e204264b144d4c929d126d0de524b40a961686414bab5040f7465c71be" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" -dependencies = [ - "sha2", - "walkdir", -] - -[[package]] -name = "rust-ini" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" -dependencies = [ - "cfg-if", - "ordered-multimap", - "trim-in-place", -] - -[[package]] -name = "rust-s3" -version = "0.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3df3f353b1f4209dcf437d777cda90279c397ab15a0cd6fd06bd32c88591533" -dependencies = [ - "async-trait", - "aws-creds", - "aws-region", - "base64", - "bytes", - "cfg-if", - "futures", - "hex", - "hmac", - "http 0.2.12", - "hyper 0.14.32", - "hyper-tls 0.5.0", - "log", - "maybe-async", - "md5", - "minidom", - "native-tls", - "percent-encoding", - "quick-xml", - "serde", - "serde_derive", - "serde_json", - "sha2", - "thiserror 1.0.69", - "time", - "tokio", - "tokio-native-tls", - "tokio-stream", - "url", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "rxml" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98f186c7a2f3abbffb802984b7f1dfd65dac8be1aafdaabbca4137f53f0dff7" -dependencies = [ - "bytes", - "rxml_validation", - "smartstring", -] - -[[package]] -name = "rxml_validation" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a197350ece202f19a166d1ad6d9d6de145e1d2a8ef47db299abe164dbd7530" - -[[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 = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - -[[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 = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[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_plain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" -dependencies = [ - "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 = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" -dependencies = [ - "serde", -] - -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" -dependencies = [ - "base64", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.2", - "hashlink", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror 2.0.12", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn", - "tempfile", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.5", - "rsa", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.12", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand 0.8.5", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.12", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror 2.0.12", - "tracing", - "url", - "uuid", -] - -[[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 = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[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 = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[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.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" -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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -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.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" -dependencies = [ - "cfg-if", - "fastrand", - "getrandom 0.3.1", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "time" -version = "0.3.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" - -[[package]] -name = "time-macros" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "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-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "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", -] - -[[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.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "trim-in-place" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" - -[[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 = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.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 = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[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 = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "utoipa" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" -dependencies = [ - "indexmap", - "serde", - "serde_json", - "utoipa-gen", -] - -[[package]] -name = "utoipa-actix-web" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f" -dependencies = [ - "actix-service", - "actix-web", - "utoipa", -] - -[[package]] -name = "utoipa-gen" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn", - "uuid", -] - -[[package]] -name = "utoipa-swagger-ui" -version = "9.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29519b3c485df6b13f4478ac909a491387e9ef70204487c3b64b53749aec0be" -dependencies = [ - "actix-web", - "base64", - "mime_guess", - "regex", - "rust-embed", - "serde", - "serde_json", - "url", - "utoipa", - "zip", -] - -[[package]] -name = "uuid" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" -dependencies = [ - "getrandom 0.3.1", - "serde", -] - -[[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.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" -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 = "webpki-roots" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "whoami" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" -dependencies = [ - "redox_syscall", - "wasite", -] - -[[package]] -name = "winapi-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 = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.53.0", -] - -[[package]] -name = "windows-result" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive 0.8.24", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -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 = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zip" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "flate2", - "indexmap", - "memchr", - "zopfli", -] - -[[package]] -name = "zopfli" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/api/Cargo.toml b/api/Cargo.toml index fa836c0..c876077 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -30,9 +30,15 @@ rust-s3 = "0.35.1" rand = "0.9.1" rand_chacha = "0.9.0" futures = "0.3.31" -utoipa = { version = "5.3.1", features = ["chrono", "uuid", "actix_extras"] } -utoipa-swagger-ui = { version = "9.0.1", features = ["actix-web"] } -utoipa-actix-web = "0.1.2" +#utoipa = { version = "5.3.1", features = ["chrono", "uuid", "actix_extras"] } +#utoipa-swagger-ui = { version = "9.0.1", features = ["actix-web"] } +#utoipa-actix-web = "0.1.2" +# Temporary fix until crate is updated to fix zip yank +utoipa = { git = "https://github.com/juhaku/utoipa.git", rev = "cecda0531bf7d90800af66b186055932ee730526", features = ["chrono", "uuid", "actix_extras"] } +utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa.git", rev = "cecda0531bf7d90800af66b186055932ee730526", features = ["actix-web"] } +utoipa-actix-web = { git = "https://github.com/juhaku/utoipa.git", rev = "cecda0531bf7d90800af66b186055932ee730526" } webpki-roots = "1.0.0" lettre = { version = "0.11.16", features = ["builder", "smtp-transport", "tokio1-native-tls"] } handlebars = "6.3.2" +governor = "0.10.0" +flate2 = "1.1.1" diff --git a/api/migrations/20250513_initial.sql b/api/migrations/20250513_initial.sql index 928e09e..88e7cae 100644 --- a/api/migrations/20250513_initial.sql +++ b/api/migrations/20250513_initial.sql @@ -65,8 +65,8 @@ CREATE TABLE IF NOT EXISTS metars ( CREATE INDEX ON metars (observation_time DESC); CREATE TABLE IF NOT EXISTS users ( - id UUID UNIQUE NOT NULL, - email TEXT NOT NULL, + username TEXT PRIMARY KEY NOT NULL, + email TEXT, email_verified BOOLEAN NOT NULL DEFAULT false, password_hash TEXT NOT NULL, role TEXT NOT NULL, @@ -74,6 +74,5 @@ CREATE TABLE IF NOT EXISTS users ( last_name TEXT NOT NULL, avatar TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY(email) + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/api/src/account/auth.rs b/api/src/account/auth.rs index 1c6fc93..1fb23cd 100644 --- a/api/src/account/auth.rs +++ b/api/src/account/auth.rs @@ -34,13 +34,13 @@ impl FromRequest for Auth { return Err(Error::new(401, "API Key does not exist".to_string()).into()); } }; - match User::select(&api_key.user_id).await { + match User::select(&api_key.username).await { Some(user) => Ok(Auth { session_id: None, api_key: Some(key_id), user, }), - None => Err(Error::new(404, format!("User {} not found", api_key.user_id)).into()), + None => Err(Error::new(404, format!("User {} not found", api_key.username)).into()), } }; return Box::pin(fut); @@ -79,13 +79,13 @@ impl FromRequest for Auth { // Verify the session let fut = async move { match Session::verify(&session_id, &ip_address).await { - Ok(session) => match User::select(&session.user_id).await { + Ok(session) => match User::select(&session.username).await { Some(user) => Ok(Auth { session_id: Some(session_id), api_key: None, user, }), - None => Err(Error::new(404, format!("User {} not found", session.user_id)).into()), + None => Err(Error::new(404, format!("User {} not found", session.username)).into()), }, Err(err) => Err(err.into()), } diff --git a/api/src/account/email_token.rs b/api/src/account/email_token.rs index 7a95de9..69d8393 100644 --- a/api/src/account/email_token.rs +++ b/api/src/account/email_token.rs @@ -1,4 +1,4 @@ -use crate::account::hash; +use crate::account::{csprng, hash}; use crate::db::redis_async_connection; use crate::error::{ApiResult, Error}; use crate::smtp; @@ -66,7 +66,7 @@ pub struct SimpleEmailCtx { pub year: i32, } -pub fn send_password_reset_email( +pub async fn send_password_reset_email( email: &str, email_token: &EmailToken, ip_address: &str, @@ -99,7 +99,7 @@ pub fn send_password_reset_email( .render_template(&template_html, &ctx) .unwrap(); - match smtp::send_email(&email, subject, plain, html) { + match smtp::send_email(&email, subject, plain, html).await { Ok(_) => Ok(()), Err(err) => { log::error!( @@ -113,11 +113,11 @@ pub fn send_password_reset_email( } } -pub fn send_confirm_email( - email: &str, - email_token: &EmailToken, - ip_address: &str, -) -> ApiResult<()> { +pub async fn send_confirm_email(email: &str, ip_address: &str) -> ApiResult<()> { + let token = csprng(128); + let email_token = EmailToken::new(email.to_string(), token, &ip_address); + email_token.store(86400).await?; + let base_url = env::var("EXTERNAL_URL")?; let link = format!("{base_url}/profile/confirm?token={}", email_token.token); let subject = "Confirm your email address"; @@ -146,16 +146,16 @@ pub fn send_confirm_email( .render_template(&template_html, &ctx) .unwrap(); - match smtp::send_email(&email, subject, plain, html) { - Ok(_) => Ok(()), - Err(err) => { - log::error!( - "Invalid email confirmation attempt [Email: {}] [IP Address: {}]: {}", - email, - ip_address, - err - ); - Err(err.into()) - } + if let Err(err) = smtp::send_email(&email, subject, plain, html).await { + log::error!( + "Invalid email confirmation attempt [Email: {}] [IP Address: {}]: {}", + email, + ip_address, + err + ); + let _ = EmailToken::delete(&email_token.token); + return Err(err); } + + Ok(()) } diff --git a/api/src/account/mod.rs b/api/src/account/mod.rs index eb4d8cb..ec04480 100644 --- a/api/src/account/mod.rs +++ b/api/src/account/mod.rs @@ -10,6 +10,7 @@ mod auth; mod email_token; mod routes; mod session; +mod model; pub use auth::*; pub use routes::init_routes; diff --git a/api/src/account/model.rs b/api/src/account/model.rs new file mode 100644 index 0000000..ddf6331 --- /dev/null +++ b/api/src/account/model.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PasswordRequirements { + pub max_length: Option, + pub min_length: Option, + pub lowercase_count: Option, + pub uppercase_count: Option, + pub numeric_count: Option, + pub special_count: Option, +} + +impl Default for PasswordRequirements { + fn default() -> Self { + Self { + max_length: Some(128), + min_length: Some(6), + lowercase_count: None, + uppercase_count: None, + numeric_count: None, + special_count: None, + } + } +} diff --git a/api/src/account/routes.rs b/api/src/account/routes.rs index 98d5730..5118efd 100644 --- a/api/src/account/routes.rs +++ b/api/src/account/routes.rs @@ -5,7 +5,6 @@ use crate::{ }; use actix_web::{HttpRequest, HttpResponse, ResponseError, get, post, put, web}; use serde::Deserialize; -use std::fs; use utoipa::ToSchema; use utoipa_actix_web::scope; use utoipa_actix_web::service_config::ServiceConfig; @@ -15,7 +14,7 @@ use crate::account::{Auth, csprng}; use crate::users::UpdateUser; #[utoipa::path( - tag = "Account", + tag = "account", request_body( content = RegisterRequest, content_type = "application/json" ), @@ -27,6 +26,7 @@ use crate::users::UpdateUser; #[post("/register")] async fn register(user: web::Json, req: HttpRequest) -> HttpResponse { let register_user = user.into_inner(); + let username = register_user.username.clone(); let email = register_user.email.clone(); let ip_address = req.peer_addr().unwrap().ip().to_string(); let insert_user: User = match register_user.to_user() { @@ -38,20 +38,19 @@ async fn register(user: web::Json, req: HttpRequest) -> HttpRes Ok(user) => { let user_response: UserResponse = user.into(); log::info!( - "Successful user registration [Email: {}] [IP Address: {}]", - email, + "Successful user registration [User: {}] [IP Address: {}]", + username, ip_address ); // Send confirmation email - let token = csprng(128); - let email_token = EmailToken::new(email.clone(), token, &ip_address); - if let Err(err) = email_token.store(86400).await { - return ResponseError::error_response(&err); + if let Some(email) = email { + tokio::spawn(async move { + if let Err(err) = send_confirm_email(&email, &ip_address).await { + log::error!("Failed to send confirmation email: {}", err); + }; + }); } - if let Err(err) = send_confirm_email(&email, &email_token, &ip_address) { - return ResponseError::error_response(&Error::new(500, err.to_string())); - }; HttpResponse::Created().json(user_response) } @@ -59,21 +58,133 @@ async fn register(user: web::Json, req: HttpRequest) -> HttpRes // Obfuscate the service error message to prevent leaking database details if err.status == 409 { log::warn!( - "Duplicate user registration attempt [Email: {}] [IP Address: {}]", - email, + "Duplicate user registration attempt [User: {}] [IP Address: {}]", + username, ip_address ); HttpResponse::Conflict().finish() } else { - log::error!("Failed to register user [Email: {}]: {}", email, err); + log::error!("Failed to register user [User: {}]: {}", username, err); ResponseError::error_response(&err) } } } } +#[derive(Debug, Deserialize, ToSchema)] +struct ConfirmEmail { + token: String, +} + #[utoipa::path( - tag = "Account", + tag = "account", + request_body( + content = ConfirmEmail, content_type = "application/json" + ), + responses( + (status = 200, description = "Successful Response", body = UserResponse), + (status = 404, description = "Not Found"), + (status = 409, description = "Conflict"), + ), +)] +#[post("/register/confirm")] +async fn confirm_email_registration( + request: web::Json, + req: HttpRequest, +) -> HttpResponse { + let ip_address = req.peer_addr().unwrap().ip().to_string(); + let token = &request.token; + + let email_token = match EmailToken::get(token).await { + Ok(password_reset) => { + if let Err(err) = EmailToken::delete(&password_reset.token).await { + return ResponseError::error_response(&err); + }; + password_reset + } + Err(_) => { + return HttpResponse::NotFound().finish(); + } + }; + + match User::select_by_email(&email_token.email).await { + Some(user) => { + let update_user = UpdateUser { + email: None, + email_verified: Some(true), + password: None, + role: None, + first_name: None, + last_name: None, + avatar: None, + }; + + match update_user.update(&user.username).await { + Ok(user) => { + let response: UserResponse = user.into(); + log::info!( + "Successful email confirmation attempt [Email: {}] [IP Address: {}]", + &email_token.email, + ip_address + ); + HttpResponse::Ok().json(response) + } + Err(err) => { + log::error!( + "Invalid email confirmation attempt [Email: {}] [IP Address: {}]: {}", + &email_token.email, + ip_address, + err + ); + ResponseError::error_response(&err) + } + } + } + None => HttpResponse::NotFound().finish(), + } +} + +#[utoipa::path( + tag = "account", + responses( + (status = 200, description = "Successful Response"), + (status = 404, description = "Not Found"), + ), + security( + ("session_auth" = []) + ) +)] +#[post("/register/email")] +async fn resend_email_verification(req: HttpRequest, auth: Auth) -> HttpResponse { + let email = auth.user.email; + let ip_address = req.peer_addr().unwrap().ip().to_string(); + + match email { + Some(email) => { + let user = match User::select_by_email(&email).await { + Some(query_user) => query_user, + None => return HttpResponse::Unauthorized().finish(), + }; + + // Cannot reverify if user is already verified + if user.email_verified { + return HttpResponse::Conflict().finish(); + } + + // Send reverify confirmation email + tokio::spawn(async move { + if let Err(err) = send_confirm_email(&email, &ip_address).await { + log::error!("Failed to send reverify confirmation email: {}", err); + }; + }); + HttpResponse::Ok().finish() + } + None => HttpResponse::NotFound().finish(), + } +} + +#[utoipa::path( + tag = "account", request_body( content = LoginRequest, content_type = "application/json" ), @@ -83,32 +194,32 @@ async fn register(user: web::Json, req: HttpRequest) -> HttpRes )] #[post("/login")] async fn login(request: web::Json, req: HttpRequest) -> HttpResponse { - let email = &request.email; + let username = &request.username; let ip_address = req.peer_addr().unwrap().ip().to_string(); - let query_user = match User::select_by_email(&email).await { + let query_user = match User::select(&username).await { Some(query_user) => query_user, None => return HttpResponse::Unauthorized().finish(), }; if verify_hash(&request.password, &query_user.password_hash) { // Create a session - let session = Session::default(&query_user.id, &ip_address); + let session = Session::default(&query_user.username, &ip_address); let session_cookie = session.cookie(); let session_exp_cookie = session.expiration_cookie(); // Save the session to the database if let Err(err) = session.store().await { log::error!( - "Login attempt failure [Email: {}] [IP Address: {}]: {}", - email, + "Login attempt failure [User: {}] [IP Address: {}]: {}", + username, ip_address, err ); return ResponseError::error_response(&Error::new(500, err.to_string())); } log::info!( - "Successful login attempt [Email: {}] [IP Address: {}]", - email, + "Successful login attempt [User: {}] [IP Address: {}]", + username, ip_address ); let user_response: UserResponse = query_user.into(); @@ -118,8 +229,8 @@ async fn login(request: web::Json, req: HttpRequest) -> HttpRespon .json(user_response) } else { log::error!( - "Invalid login attempt [Email: {}] [IP Address: {}]", - email, + "Invalid login attempt [User: {}] [IP Address: {}]", + username, ip_address ); HttpResponse::Unauthorized() @@ -130,7 +241,7 @@ async fn login(request: web::Json, req: HttpRequest) -> HttpRespon } #[utoipa::path( - tag = "Account", + tag = "account", responses( (status = 200, description = "Successful Response"), (status = 401, description = "Unauthorized"), @@ -141,7 +252,7 @@ async fn login(request: web::Json, req: HttpRequest) -> HttpRespon )] #[post("/logout")] async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse { - let email = auth.user.email; + let username = auth.user.username; let ip_address = req.peer_addr().unwrap().ip().to_string(); // Delete the session from the store match req.cookie(SESSION_COOKIE_NAME) { @@ -149,8 +260,8 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse { let session_id = cookie.value().to_string(); if let Err(err) = Session::delete(&session_id).await { log::error!( - "Logout attempt failure [Email: {}] [IP Address: {}]: {}", - email, + "Logout attempt failure [User: {}] [IP Address: {}]: {}", + username, ip_address, err ); @@ -159,8 +270,8 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse { } None => { log::error!( - "Invalid logout attempt [Email: {}] [IP Address: {}]", - email, + "Invalid logout attempt [User: {}] [IP Address: {}]", + username, ip_address ); return ResponseError::error_response(&Error::new(400, "Invalid session".to_string())); @@ -168,8 +279,8 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse { } log::info!( - "Successful logout attempt [Email: {}] [IP Address: {}]", - email, + "Successful logout attempt [User: {}] [IP Address: {}]", + username, ip_address ); HttpResponse::Ok() @@ -179,7 +290,7 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse { } #[utoipa::path( - tag = "Account", + tag = "account", responses( (status = 200, description = "Successful Response", body = UserResponse), (status = 401, description = "Unauthorized"), @@ -210,8 +321,8 @@ async fn get_profile(req: HttpRequest) -> HttpResponse { .finish(); } }; - let id = &session.user_id; - let query_user = match User::select(&id).await { + let username = &session.username; + let query_user = match User::select(&username).await { Some(query_user) => query_user, None => { return HttpResponse::Unauthorized() @@ -226,8 +337,8 @@ async fn get_profile(req: HttpRequest) -> HttpResponse { let session_exp_cookie = session.expiration_cookie(); log::info!( - "Successful profile attempt [ID: {}] [IP Address: {}]", - id, + "Successful profile attempt [User: {}] [IP Address: {}]", + username, ip_address ); HttpResponse::Ok() @@ -242,77 +353,8 @@ async fn get_profile(req: HttpRequest) -> HttpResponse { } } -#[derive(Debug, Deserialize, ToSchema)] -struct TokenRequest { - token: String, -} - #[utoipa::path( - tag = "Account", - request_body( - content = TokenRequest, content_type = "application/json" - ), - responses( - (status = 200, description = "Successful Response", body = UserResponse), - (status = 404, description = "Not Found"), - ), -)] -#[post("/profile/confirm")] -async fn confirm_profile(request: web::Json, req: HttpRequest) -> HttpResponse { - let ip_address = req.peer_addr().unwrap().ip().to_string(); - let token = &request.token; - - let email_token = match EmailToken::get(token).await { - Ok(password_reset) => { - if let Err(err) = EmailToken::delete(&password_reset.token).await { - return ResponseError::error_response(&err); - }; - password_reset - } - Err(_) => { - return HttpResponse::NotFound().finish(); - } - }; - - match User::select_by_email(&email_token.email).await { - Some(user) => { - let update_user = UpdateUser { - email: None, - email_verified: Some(true), - password: None, - role: None, - first_name: None, - last_name: None, - avatar: None, - }; - - match update_user.update(&user.id).await { - Ok(user) => { - let response: UserResponse = user.into(); - log::info!( - "Successful email confirmation attempt [Email: {}] [IP Address: {}]", - &email_token.email, - ip_address - ); - HttpResponse::Ok().json(response) - } - Err(err) => { - log::error!( - "Invalid email confirmation attempt [Email: {}] [IP Address: {}]: {}", - &email_token.email, - ip_address, - err - ); - ResponseError::error_response(&err) - } - } - } - None => HttpResponse::NotFound().finish(), - } -} - -#[utoipa::path( - tag = "Account", + tag = "account", responses( (status = 200, description = "Successful Response"), (status = 401, description = "Unauthorized"), @@ -343,13 +385,13 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse { .finish(); } }; - let id = &session.user_id; + let username = &session.username; let session_cookie = session.cookie(); let session_exp_cookie = session.expiration_cookie(); log::info!( - "Successful session validate attempt [ID: {}] [IP Address: {}]", - id, + "Successful session validate attempt [User: {}] [IP Address: {}]", + username, ip_address ); HttpResponse::Ok() @@ -365,14 +407,14 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse { } #[derive(Debug, Deserialize, ToSchema)] -struct PasswordRequest { +struct ChangePassword { password: String, } #[utoipa::path( - tag = "Account", + tag = "account", request_body( - content = PasswordRequest, content_type = "application/json" + content = ChangePassword, content_type = "application/json" ), responses( (status = 200, description = "Successful Response", body = UserResponse), @@ -384,14 +426,14 @@ struct PasswordRequest { )] #[put("/password")] async fn change_password( - request: web::Json, + request: web::Json, req: HttpRequest, auth: Auth, ) -> HttpResponse { let ip_address = req.peer_addr().unwrap().ip().to_string(); - let id = auth.user.id; + let username = auth.user.username; - if let None = User::select(&id).await { + if let None = User::select(&username).await { return HttpResponse::Unauthorized().finish(); }; @@ -405,20 +447,20 @@ async fn change_password( avatar: None, }; - match update_user.update(&id).await { + match update_user.update(&username).await { Ok(user) => { let response: UserResponse = user.into(); log::info!( - "Successful password change attempt [ID: {}] [IP Address: {}]", - &id, + "Successful password change attempt [User: {}] [IP Address: {}]", + &username, ip_address ); HttpResponse::Ok().json(response) } Err(err) => { log::error!( - "Invalid password change attempt [ID: {}] [IP Address: {}]: {}", - &id, + "Invalid password change attempt [User: {}] [IP Address: {}]: {}", + &username, ip_address, err ); @@ -428,26 +470,26 @@ async fn change_password( } #[derive(Debug, Deserialize, ToSchema)] -struct EmailRequest { +struct PasswordReset { email: String, } #[utoipa::path( - tag = "Account", + tag = "account", request_body( - content = EmailRequest, content_type = "application/json" + content = PasswordReset, content_type = "application/json" ), responses( (status = 200, description = "Successful Response"), ) )] #[post("/password/reset")] -async fn reset_password(request: web::Json, req: HttpRequest) -> HttpResponse { +async fn reset_password(request: web::Json, req: HttpRequest) -> HttpResponse { let email = &request.email; let ip_address = req.peer_addr().unwrap().ip().to_string(); let token = csprng(128); - // Silently return if the user does not exist + // Silently return if the user's email does not exist if let None = User::select_by_email(&email).await { return HttpResponse::Ok().finish(); }; @@ -457,27 +499,34 @@ async fn reset_password(request: web::Json, req: HttpRequest) -> H return ResponseError::error_response(&err); } - if let Err(err) = send_password_reset_email(email, &email_token, &ip_address) { + if let Err(err) = send_password_reset_email(email, &email_token, &ip_address).await { return ResponseError::error_response(&Error::new(500, err.to_string())); }; HttpResponse::Ok().finish() } +#[derive(Debug, Deserialize, ToSchema)] +struct ConfirmPasswordReset { + token: String, + password: String, +} + #[utoipa::path( - tag = "Account", + tag = "account", request_body( - content = TokenRequest, content_type = "application/json" + content = ConfirmPasswordReset, content_type = "application/json" ), responses( (status = 200, description = "Successful Response"), (status = 404, description = "Not Found"), ) )] -#[post("/password/validate")] -async fn validate_reset_password( - request: web::Json, +#[post("/password/reset/confirm")] +async fn confirm_password_reset( + request: web::Json, req: HttpRequest, ) -> HttpResponse { + // TODO let ip_address = req.peer_addr().unwrap().ip().to_string(); let token = &request.token; @@ -500,13 +549,14 @@ pub fn init_routes(config: &mut ServiceConfig) { config.service( scope::scope("/account") .service(register) + .service(confirm_email_registration) + .service(resend_email_verification) .service(login) .service(logout) .service(get_profile) - .service(confirm_profile) .service(session_refresh) .service(change_password) .service(reset_password) - .service(validate_reset_password), + .service(confirm_password_reset), ); } diff --git a/api/src/account/session.rs b/api/src/account/session.rs index ba0a8c9..59c8840 100644 --- a/api/src/account/session.rs +++ b/api/src/account/session.rs @@ -8,7 +8,6 @@ use chrono::{DateTime, Utc}; use redis::{AsyncCommands, RedisResult}; use serde::{Deserialize, Serialize}; use tokio::task; -use uuid::Uuid; const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours pub const SESSION_COOKIE_NAME: &str = "session"; @@ -17,22 +16,22 @@ pub const SESSION_EXPIRATION_COOKIE_NAME: &str = "session_expiration"; #[derive(Debug, Serialize, Deserialize)] pub struct Session { pub session_id: String, - pub user_id: Uuid, + pub username: String, pub ip_address: String, #[serde(skip_serializing_if = "Option::is_none")] pub expires_at: Option>, } impl Session { - pub fn default(user_id: &Uuid, ip_address: &str) -> Self { - Self::new(64, user_id, ip_address, Some(DEFAULT_SESSION_TTL)) + pub fn default(username: &str, ip_address: &str) -> Self { + Self::new(64, username, ip_address, Some(DEFAULT_SESSION_TTL)) } - pub fn new(take: usize, user_id: &Uuid, ip_address: &str, ttl: Option) -> Self { + pub fn new(take: usize, username: &str, ip_address: &str, ttl: Option) -> Self { let now = Utc::now(); Self { session_id: csprng(take), - user_id: user_id.clone(), + username: username.to_string(), ip_address: hash(&ip_address).unwrap(), expires_at: match ttl { Some(ttl) => Some(now + chrono::Duration::seconds(ttl)), @@ -79,7 +78,7 @@ impl Session { ); }; }); - session = Session::default(&session.user_id, ip_address); + session = Session::default(&session.username, ip_address); session.store().await?; Ok(session) } @@ -120,8 +119,8 @@ impl Session { if let Ok(environment) = std::env::var("ENVIRONMENT") { if environment == "development" || environment == "dev" { log::trace!( - "Session cookie [User ID: {}]: {}", - self.user_id, + "Session cookie [User: {}]: {}", + self.username, self.session_id ); cookie.set_secure(false); @@ -148,8 +147,8 @@ impl Session { if let Ok(environment) = std::env::var("ENVIRONMENT") { if environment == "development" || environment == "dev" { log::trace!( - "Session expiration cookie [User ID: {}]: {}", - self.user_id, + "Session expiration cookie [User: {}]: {}", + self.username, self.session_id ); cookie.set_secure(false); diff --git a/api/src/airports/model/airport.rs b/api/src/airports/model/airport.rs index 48fad5e..2069381 100644 --- a/api/src/airports/model/airport.rs +++ b/api/src/airports/model/airport.rs @@ -7,7 +7,6 @@ use crate::error::{ApiResult, Error}; use crate::metars::Metar; use chrono::{DateTime, Utc}; use futures_util::try_join; -use reqwest::Client; use serde::{Deserialize, Serialize}; use sqlx::{Postgres, QueryBuilder}; use std::collections::HashMap; @@ -81,6 +80,54 @@ impl Default for AirportQuery { } } +impl AirportQuery { + pub fn builder() -> AirportQueryBuilder { + AirportQueryBuilder::new() + } +} + +pub struct AirportQueryBuilder { + inner: AirportQuery, +} + +impl AirportQueryBuilder { + /// start the builder + pub fn new() -> Self { + AirportQueryBuilder { + inner: AirportQuery::default(), + } + } + + pub fn page(mut self, page: u32) -> Self { + self.inner.page = Some(page); + self + } + + pub fn limit(mut self, limit: u32) -> Self { + self.inner.limit = Some(limit); + self + } + + pub fn icaos>(mut self, v: T) -> Self { + self.inner.icaos = Some(v.into()); + self + } + + pub fn iatas>(mut self, v: T) -> Self { + self.inner.iatas = Some(v.into()); + self + } + + pub fn metars(mut self, v: bool) -> Self { + self.inner.metars = Some(v); + self + } + + pub fn build(self) -> AirportQuery { + self.inner + } +} + #[derive(Debug, Deserialize, ToSchema)] pub struct Bounds { pub north_east_lat: f32, @@ -208,7 +255,7 @@ impl From for Airport { } impl Airport { - pub async fn select(client: &Client, icao: &str, metar: bool) -> Option { + pub async fn select(icao: &str, metar: bool) -> Option { let pool = db::pool(); let airport_fut = async { @@ -223,7 +270,7 @@ impl Airport { let metar_fut = async { if metar { - match Metar::find_all_distinct(client, &vec![icao.to_string()]).await { + match Metar::get_all_distinct(&vec![icao.to_uppercase()]).await { Ok(m) => Some(m.into_iter().nth(0)), Err(err) => { log::error!("{}", err); @@ -286,7 +333,7 @@ impl Airport { }) } - pub async fn select_all(client: &Client, query: &AirportQuery) -> ApiResult> { + pub async fn select_all(query: &AirportQuery) -> ApiResult> { let pool = db::pool(); let mut builder = @@ -349,12 +396,12 @@ impl Airport { } // Bulk update airport sub-fields - let icaos: Vec = airports.iter().map(|a| a.icao.clone()).collect(); + let icaos: Vec = airports.iter().map(|a| a.icao.to_uppercase()).collect(); - let runway_future = Runway::select_all_map(icaos.clone()); - let frequency_future = Communication::select_all_map(icaos.clone()); + let runway_future = Runway::select_all_map(&icaos); + let frequency_future = Communication::select_all_map(&icaos); let metar_future = if query.metars.unwrap_or(false) { - Some(Metar::find_all_distinct(client, &icaos)) + Some(Metar::get_all_distinct(&icaos)) } else { None }; diff --git a/api/src/airports/model/communication.rs b/api/src/airports/model/communication.rs index b5bcef2..20cca0a 100644 --- a/api/src/airports/model/communication.rs +++ b/api/src/airports/model/communication.rs @@ -65,7 +65,7 @@ impl Communication { } } - pub async fn select_all_map(icaos: Vec) -> ApiResult>> { + pub async fn select_all_map(icaos: &Vec) -> ApiResult>> { let pool = db::pool(); let frequency_rows: Vec = sqlx::query_as(&format!( diff --git a/api/src/airports/model/runway.rs b/api/src/airports/model/runway.rs index 223f114..f8685d4 100644 --- a/api/src/airports/model/runway.rs +++ b/api/src/airports/model/runway.rs @@ -64,7 +64,7 @@ impl Runway { } } - pub async fn select_all_map(icaos: Vec) -> ApiResult>> { + pub async fn select_all_map(icaos: &Vec) -> ApiResult>> { let pool = db::pool(); let runway_rows: Vec = sqlx::query_as(&format!( diff --git a/api/src/airports/routes.rs b/api/src/airports/routes.rs index ded3317..be5f1a8 100644 --- a/api/src/airports/routes.rs +++ b/api/src/airports/routes.rs @@ -3,7 +3,6 @@ use futures_util::stream::StreamExt as _; use crate::airports::{AirportQuery, UpdateAirport}; use crate::users::ADMIN_ROLE; use crate::{ - AppState, account::{Auth, verify_role}, airports::Airport, db::Paged, @@ -16,15 +15,15 @@ use utoipa_actix_web::service_config::ServiceConfig; #[derive(ToSchema)] #[allow(unused)] -struct UploadedFile { +struct FileUpload { #[schema(value_type = String, format = Binary)] file: Vec, } #[utoipa::path( - tag = "Airports", + tag = "airport", request_body( - content = UploadedFile, content_type = "multipart/form-data" + content = FileUpload, content_type = "multipart/form-data" ), responses( (status = 200, description = "Successful import"), @@ -77,7 +76,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse { } #[utoipa::path( - tag = "Airports", + tag = "airport", params( AirportQuery ), @@ -86,7 +85,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse { ), )] #[get("")] -async fn get_airports(data: web::Data, req: HttpRequest) -> HttpResponse { +async fn get_airports(req: HttpRequest) -> HttpResponse { let mut query = match web::Query::::from_query(req.query_string()) { Ok(q) => q.into_inner(), Err(err) => { @@ -104,8 +103,7 @@ async fn get_airports(data: web::Data, req: HttpRequest) -> HttpRespon query.limit = Some(limit); query.page = Some(page); - let client = &data.client; - match Airport::select_all(client, &query).await { + match Airport::select_all(&query).await { Ok(airports) => HttpResponse::Ok().json(Paged { data: airports, page, @@ -120,18 +118,14 @@ async fn get_airports(data: web::Data, req: HttpRequest) -> HttpRespon } #[utoipa::path( - tag = "Airports", + tag = "airport", responses( (status = 200, description = "", body = Airport), (status = 404, description = ""), ), )] #[get("/{icao}")] -async fn get_airport( - data: web::Data, - icao: web::Path, - req: HttpRequest, -) -> HttpResponse { +async fn get_airport(icao: web::Path, req: HttpRequest) -> HttpResponse { let metar = match web::Query::::from_query(req.query_string()) { Ok(q) => q.metars.unwrap_or_else(|| false), Err(err) => { @@ -140,15 +134,14 @@ async fn get_airport( } }; - let client = &data.client; - match Airport::select(client, &icao.into_inner(), metar).await { + match Airport::select(&icao.into_inner(), metar).await { Some(airport) => HttpResponse::Ok().json(airport), None => HttpResponse::NotFound().finish(), } } #[utoipa::path( - tag = "Airports", + tag = "airport", responses( (status = 200, description = "", body = Airport), (status = 401, description = ""), @@ -174,7 +167,7 @@ async fn insert_airport(airport: web::Json, auth: Auth) -> HttpResponse } #[utoipa::path( - tag = "Airports", + tag = "airport", responses( (status = 200, description = "", body = Airport), (status = 401, description = ""), @@ -203,7 +196,7 @@ async fn update_airport( } #[utoipa::path( - tag = "Airports", + tag = "airport", responses( (status = 201, description = ""), (status = 401, description = ""), @@ -228,7 +221,7 @@ async fn delete_airports(auth: Auth) -> HttpResponse { } #[utoipa::path( - tag = "Airports", + tag = "airport", responses( (status = 201, description = ""), (status = 401, description = ""), diff --git a/api/src/http_client.rs b/api/src/http_client.rs new file mode 100644 index 0000000..f8cabdd --- /dev/null +++ b/api/src/http_client.rs @@ -0,0 +1,91 @@ +use crate::error::{ApiResult, Error}; +use governor::clock::DefaultClock; +use governor::state::{InMemoryState, NotKeyed}; +use governor::{Quota, RateLimiter}; +use reqwest::header::{IF_NONE_MATCH, RETRY_AFTER}; +use reqwest::{Certificate, Client, Response, StatusCode}; +use std::env; +use std::num::NonZeroU32; +use std::sync::Arc; +use std::time::Duration; +use tokio::time::sleep; + +#[derive(Debug, Clone)] +pub struct HttpClient { + client: Client, + limiter: Arc>, + pub default_retry_after: u64, +} + +impl HttpClient { + pub fn new(default_retry_after: u64) -> ApiResult { + let mut client_builder = Client::builder() + .timeout(Duration::from_secs(10)) + .tls_built_in_root_certs(true); + + if let Ok(val) = env::var("NGINX_SSL_ENABLED") { + if val == "true" { + let certificate_path = env::var("SSL_CA_PATH")?; + let certificate_data = std::fs::read(certificate_path)?; + let certificate = Certificate::from_pem(&certificate_data)?; + client_builder = client_builder.add_root_certificate(certificate); + } + } + + let client = client_builder.build()?; + + let quota = Quota::per_second(NonZeroU32::new(15).unwrap()); + let limiter = RateLimiter::direct(quota); + let limiter = Arc::new(limiter); + + Ok(Self { + client, + limiter, + default_retry_after, + }) + } + + pub fn default() -> ApiResult { + Self::new(60) + } + + pub async fn get(&self, url: &str, etag: Option) -> ApiResult { + self.limiter.until_ready().await; + + let mut request = self.client.get(url); + if let Some(ref etag) = etag { + request = request.header(IF_NONE_MATCH, etag); + } + + let mut response = request.send().await?; + + // Handle too many requests + if response.status() == StatusCode::TOO_MANY_REQUESTS { + let retry_after = response + .headers() + .get(RETRY_AFTER) + .and_then(|hdr| hdr.to_str().ok()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(self.default_retry_after); + + log::warn!( + "Received 429 Too Many Requests, retrying after {}s", + retry_after + ); + sleep(Duration::from_secs(retry_after)).await; + + // Retry once more + response = self.client.get(url).send().await?; + } else if response.status() == StatusCode::NOT_MODIFIED { + log::warn!("Received 304 Not modified") + } + + if response.status() != 200 { + return Err(Error::new( + response.status().as_u16(), + format!("Request returned status {}", response.status()))); + } + + Ok(response) + } +} diff --git a/api/src/main.rs b/api/src/main.rs index 171786d..4a55efc 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -1,21 +1,21 @@ use crate::account::hash; +use crate::http_client::HttpClient; use crate::users::{ADMIN_ROLE, User}; use actix_cors::Cors; use actix_web::{App, HttpServer, middleware::Logger, web}; use dotenv::from_filename; -use reqwest::Certificate; use std::env; -use std::time::Duration; +use std::sync::Arc; +use utoipa::openapi::SecurityRequirement; use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}; -use utoipa::openapi::{Contact, SecurityRequirement}; use utoipa_actix_web::{AppExt, scope}; use utoipa_swagger_ui::{Config, SwaggerUi}; -use uuid::Uuid; mod account; mod airports; mod db; mod error; +mod http_client; mod metars; mod scheduler; mod smtp; @@ -24,19 +24,25 @@ mod users; #[derive(Debug, Clone)] struct AppState { - client: reqwest::Client, + client: Arc, } #[actix_web::main] async fn main() -> Result<(), Box> { initialize_environment()?; db::initialize().await?; - // scheduler::update_airports(); + + let client = Arc::new(HttpClient::default()?); + + let scheduler_client = client.clone(); + scheduler::update_metars(scheduler_client, 600); // Initialize admin user + let admin_username = env::var("ADMIN_USERNAME"); let admin_email = env::var("ADMIN_EMAIL"); let admin_password = env::var("ADMIN_PASSWORD"); - if admin_email.is_ok() && admin_password.is_ok() { + if admin_username.is_ok() && admin_email.is_ok() && admin_password.is_ok() { + let username = admin_username.unwrap(); let email = admin_email.unwrap(); if User::select_by_email(&email).await.is_none() { log::debug!("Creating default administrator"); @@ -44,12 +50,12 @@ async fn main() -> Result<(), Box> { let password_hash = hash(&password)?; if email == "admin@example.com" || password == "changeme" { log::warn!( - "Default admin credentials are in use, update the ADMIN_EMAIL and ADMIN_PASSWORD." + "Default admin credentials are in use, update the ADMIN_USERNAME, ADMIN_EMAIL, and ADMIN_PASSWORD." ); } let admin_user = User { - id: Uuid::new_v4(), - email, + username, + email: Some(email), email_verified: true, password_hash, role: ADMIN_ROLE.to_string(), @@ -68,23 +74,6 @@ async fn main() -> Result<(), Box> { } } - let mut client_builder = reqwest::Client::builder() - .timeout(Duration::from_secs(10)) - .tls_built_in_root_certs(true); - - if let Ok(val) = env::var("NGINX_SSL_ENABLED") { - if val == "true" { - let certificate_path = env::var("SSL_CA_PATH")?; - let certificate_data = std::fs::read(certificate_path)?; - let certificate = Certificate::from_pem(&certificate_data)?; - client_builder = client_builder.add_root_certificate(certificate); - } - } - - let client = client_builder - .build() - .expect("Failed to create reqwest client"); - let state = AppState { client }; let host = "0.0.0.0"; let port = env::var("API_PORT").unwrap_or("5000".to_string()); @@ -111,14 +100,14 @@ async fn main() -> Result<(), Box> { ) .split_for_parts(); - let version = env::var("CARGO_PKG_VERSION").unwrap(); + let version = env!("CARGO_PKG_VERSION"); api.info.title = "Aviation Data".to_string(); - api.info.description = None; + api.info.description = Some("This documentation describe the Aviation Data API".to_string()); api.info.terms_of_service = None; api.info.contact = None; api.info.license = None; - api.info.version = version; + api.info.version = version.to_string(); let session_scheme = SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new("session"))); let mut components = api.components.take().unwrap_or_default(); @@ -126,10 +115,13 @@ async fn main() -> Result<(), Box> { .security_schemes .insert("session_auth".to_string(), session_scheme); api.components = Some(components); - // api.security = Some(vec![SecurityRequirement::new("session_auth", [""])]); api.security = Some(vec![SecurityRequirement::default()]); - app.service(SwaggerUi::new("/swagger/{_:.*}").url("/api-docs/openapi.json", api)) + app.service( + SwaggerUi::new("/swagger/{_:.*}") + .url("/api-docs/openapi.json", api) + .config(Config::default().use_base_layout()), + ) }) .bind(format!("{}:{}", host, port)) { diff --git a/api/src/metars/metar_check.rs b/api/src/metars/metar_check.rs index 5b9dbb8..d9fd1ec 100644 --- a/api/src/metars/metar_check.rs +++ b/api/src/metars/metar_check.rs @@ -35,7 +35,7 @@ impl MetarCheck { let mut conn = match redis_async_connection().await { Ok(conn) => conn, Err(err) => { - log::error!("{}", err); + log::error!("Unable to get connection for ICAO {}: {}", icao, err); return None; } }; @@ -44,24 +44,22 @@ impl MetarCheck { Ok(Some(value)) => match serde_json::from_str(&value) { Ok(result) => Some(result), Err(err) => { - log::error!("{}", err); + log::error!("Unable to get MetarCheck for ICAO {}: {}", icao, err); None } }, Ok(None) => None, Err(err) => { - log::error!("{}", err); + log::error!("Error getting MetarCheck for ICAO {}: {}", icao, err); None } } } - pub async fn insert(&self, seconds: u64) -> ApiResult<()> { + pub async fn insert(&self) -> ApiResult<()> { let mut conn = redis_async_connection().await?; let value = serde_json::to_string(&self)?; - conn - .set_ex::<_, _, ()>(self.icao.as_str(), value, seconds) - .await?; + conn.set::<_, _, ()>(self.icao.as_str(), value).await?; Ok(()) } diff --git a/api/src/metars/model.rs b/api/src/metars/model.rs index 9782066..18fe0e7 100644 --- a/api/src/metars/model.rs +++ b/api/src/metars/model.rs @@ -1,19 +1,33 @@ use crate::airports::{Airport, UpdateAirport}; -use crate::db::redis_async_connection; use crate::error::Error; +use crate::http_client::HttpClient; use crate::metars::MetarCheck; use crate::{db, error::ApiResult}; use chrono::{DateTime, Datelike, NaiveDate, Utc}; -use redis::{AsyncCommands, RedisResult}; -use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::env; use std::fmt::Display; +use std::io::{Cursor, Read}; use std::str::FromStr; +use std::sync::OnceLock; +use flate2::read::GzDecoder; +use reqwest::header::ETAG; use utoipa::ToSchema; +static TIME_OFFSET: OnceLock = OnceLock::new(); + const TABLE_NAME: &str = "metars"; +const DEFAULT_REFRESH_DURATION: i64 = 3000; + +fn time_offset() -> i64 { + *TIME_OFFSET.get_or_init(|| { + env::var("API_METAR_TIME_OFFSET") + .unwrap_or("1800".to_string()) + .parse::() + .unwrap_or(1800) + }) +} #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct Metar { @@ -292,9 +306,9 @@ impl MetarRow { impl Metar { fn parse_multiple(metar_strings: &Vec<&str>) -> ApiResult> { - let mut metars: Vec = vec![]; + let mut metars: Vec = vec![]; for metar_string in metar_strings { - match Metar::parse(metar_string) { + match Self::parse(metar_string) { Ok(metar) => metars.push(metar), Err(e) => { log::warn!("Failed to parse metar string: {}", e); @@ -315,7 +329,7 @@ impl Metar { } log::trace!("Parsing METAR data: {}", metar_string); - let mut metar: Metar = Metar::default(); + let mut metar: Self = Self::default(); metar.raw_text = metar_string.to_owned(); let mut metar_parts: Vec<&str> = metar_string.split_whitespace().collect(); if metar_parts.len() < 4 { @@ -906,9 +920,14 @@ impl Metar { observation_day ), ) - })? - .and_hms_opt(observation_hour, observation_minute, 0) - .unwrap(); + })?; + let candidate_date = match candidate_date.and_hms_opt(observation_hour, observation_minute, 0) { + Some(date) => date, + None => return Err(Error::new( + 500, + format!("Invalid time for time '{}': hour {}, minute {}", + observation_time, observation_hour, observation_minute))) + }; let obs_datetime = if candidate_date > current_time { // Subtract one month. (Handle year rollover carefully.) @@ -928,35 +947,74 @@ impl Metar { ), ) })?; - adjusted_date.and_hms(observation_hour, observation_minute, 0) + adjusted_date + .and_hms_opt(observation_hour, observation_minute, 0) + .unwrap() } else { candidate_date }; Ok(obs_datetime.format("%Y-%m-%dT%H:%M:00Z").to_string()) } - async fn get_remote_metars(client: &Client, icaos: &Vec) -> ApiResult> { - let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set"); + pub async fn get_cached_remote_metars(client: &HttpClient, etag: Option) -> ApiResult<(Vec, String)> { + let base_url = env::var("AVIATION_WEATHER_URL") + .expect("AVIATION_WEATHER_URL must be set"); + let url = format!("{}/data/cache/metars.cache.csv.gz", base_url); + + match client.get(&url, etag.clone()).await { + Ok(r) => { + let new_etag = r + .headers() + .get(ETAG) + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()); + + let bytes = r.bytes().await?; + let mut gz = GzDecoder::new(Cursor::new(bytes)); + let mut text = String::new(); + gz.read_to_string(&mut text)?; + + let mut output: Vec = Vec::new(); + + for line in text.lines() { + // Split off first column + let raw_text = line.splitn(2, ',').next().unwrap(); + match Metar::parse(raw_text) { + Ok(m) => output.push(m), + Err(err) => { + log::warn!("{}", err); + } + }; + } + + match new_etag { + Some(etag) => Ok((output, etag)), + None => match etag { + Some(etag) => Ok((output, etag)), + None => Ok((output, String::new())) + } + } + } + Err(err) => Err(err.into()), + } + } + + pub async fn get_remote_metars(client: &HttpClient, icaos: &Vec) -> ApiResult> { + let base_url = env::var("AVIATION_WEATHER_URL") + .expect("AVIATION_WEATHER_URL must be set"); // Query the remote API for the missing METAR data 10 at a time let icao_chunks = icaos .chunks(10) .map(|chunk| chunk.join(",")) .collect::>(); - let mut metars: Vec = vec![]; + let mut metars: Vec = vec![]; for icao_chunk in icao_chunks { let url = format!( - "{}/metar?ids={}&hours=0&order=id,-obs", + "{}/api/data/metar?ids={}&hours=0&order=id,-obs", base_url, icao_chunk ); - let mut m = match client.get(url).send().await { + let mut m = match client.get(&url, None).await { Ok(r) => { - // Check if the status code is 200 - if r.status() != 200 { - return Err(Error::new( - 500, - format!("Request returned status {}", r.status()), - )); - } match r.text().await { Ok(r) => { let metar_chunk = r @@ -979,22 +1037,22 @@ impl Metar { Ok(metars) } - fn from_db(metar_db: MetarRow) -> ApiResult { - let metar: Metar = serde_json::from_value(metar_db.data)?; + fn from_row(row: MetarRow) -> ApiResult { + let metar: Self = serde_json::from_value(row.data)?; Ok(metar) } - fn to_db(&self) -> ApiResult { + fn to_row(&self) -> ApiResult { let data = serde_json::to_value(self)?; Ok(MetarRow { - icao: self.icao.clone(), + icao: self.icao.to_uppercase(), observation_time: self.observation_time, raw_text: self.raw_text.clone(), data, }) } - pub async fn find_all_distinct(client: &Client, icao_list: &Vec) -> ApiResult> { + pub async fn get_all_distinct(icao_list: &Vec) -> ApiResult> { if icao_list.is_empty() { return Ok(Vec::new()); } @@ -1011,61 +1069,67 @@ impl Metar { .bind(icao_list) .fetch_all(pool) .await?; + let mut metars = vec![]; + for metar_row in metar_rows { + metars.push(Self::from_row(metar_row)?) + } + Ok(metars) + } + pub async fn get_or_update_metars( + client: &HttpClient, + icaos: &Vec, + ) -> ApiResult> { + let metars = Self::get_all_distinct(&icaos).await?; let current_time = Utc::now().timestamp(); - let time_offset = env::var("API_METAR_TIME_OFFSET") - .unwrap_or("1800".to_string()) - .parse::() - .unwrap_or(1800); - let short_time_offset: i64 = 300; - // Setup metars and missing metar structures - let mut metars: Vec = vec![]; + let mut updated_metars: Vec = vec![]; let mut missing_metar_icaos: Vec = vec![]; let mut found_metar_icaos: HashSet = HashSet::new(); - let mut requested_icaos: HashSet = HashSet::from_iter(icao_list.clone()); + let mut requested_icaos: HashSet = HashSet::from_iter(icaos.clone()); - // Iterate over returned database metars - for metar_row in metar_rows { - let icao = metar_row.icao.clone(); - // Remove icao from requested icaos + for metar in metars { + let icao = metar.icao.clone(); + // Remove found icao from requested ICAOs requested_icaos.remove(&icao); - // Handle outdated metars - if current_time > (metar_row.observation_time.timestamp() + time_offset) { + // Handle outdated METARs + if current_time > (metar.observation_time.timestamp() + time_offset()) { + // If the METAR has previously been found, get the updated_at time, otherwise default let refresh_seconds = match MetarCheck::get(&icao).await { Some(c) => current_time - c.updated_at.timestamp(), - None => short_time_offset, + None => DEFAULT_REFRESH_DURATION, }; - // If the metar was cached more than short_time_offset minutes ago, refresh it - if refresh_seconds >= short_time_offset { + + // If the metar is outdated, add it to the refresh list + if refresh_seconds >= DEFAULT_REFRESH_DURATION { log::trace!("{} METAR data is outdated, marked for refresh", &icao); missing_metar_icaos.push(icao.clone()); } - // Otherwise return outdated data and wait + // Otherwise return the outdated data (to be checked on the next cycle) else { log::trace!( "{} METAR data is outdated; refreshing in {} seconds", &icao, - short_time_offset - refresh_seconds + DEFAULT_REFRESH_DURATION - refresh_seconds ); - metars.push(Metar::from_db(metar_row)?) + updated_metars.push(metar); } } - // Otherwise add the metar to the vector + // Otherwise add the valid metar to the updated list else { found_metar_icaos.insert(icao.clone()); let metar_check = MetarCheck::new(icao, true).await; - metar_check.insert(time_offset as u64).await?; - metars.push(Metar::from_db(metar_row)?); + metar_check.insert().await?; + updated_metars.push(metar); } } - // Add all metars that were not in the returned database metars + // Add all METARs that were not in the returned database METARs for icao in &requested_icaos { match MetarCheck::get(icao).await { Some(c) => { - if current_time > (c.updated_at.timestamp() + short_time_offset) { + if current_time > (c.updated_at.timestamp() + DEFAULT_REFRESH_DURATION) { missing_metar_icaos.push(icao.to_string()); } } @@ -1075,6 +1139,7 @@ impl Metar { } } + // Retrieve missing METARs if !missing_metar_icaos.is_empty() { log::trace!( "Retrieving missing METAR data for {:?}", @@ -1087,38 +1152,47 @@ impl Metar { vec![] }); + // Insert missing METARs if remote_metars.len() > 0 { - // Insert missing METARs for remote_metar in remote_metars.clone() { remote_metar.insert().await?; found_metar_icaos.insert(remote_metar.icao.to_string()); let mut metar_check = MetarCheck::new(remote_metar.icao.clone(), true).await; metar_check.last_metar = Some(remote_metar); - metar_check.insert(time_offset as u64).await?; + metar_check.insert().await?; } - metars.append(&mut remote_metars); + updated_metars.append(&mut remote_metars); } - // Update still missing metars - // let mut still_missing_metar_icaos: Vec = vec![]; + // Update still missing METARs for difference in found_metar_icaos.symmetric_difference(&requested_icaos) { - // still_missing_metar_icaos.push(difference.to_string()); let metar_check = MetarCheck::new(difference.to_string(), false).await; - metar_check.insert(short_time_offset as u64).await?; + metar_check.insert().await?; // Only add cached metar data if it's less than 4 hours old if let Some(last_metar) = metar_check.last_metar { let four_hours_ago = Utc::now() - chrono::Duration::hours(4); if last_metar.observation_time < four_hours_ago { - metars.push(last_metar); + updated_metars.push(last_metar); } } } - // if !still_missing_metar_icaos.is_empty() { - // log::trace!("Still missing METAR data from {:?}", still_missing_metar_icaos); - // } } - Ok(metars) + Ok(updated_metars) + } + + pub async fn update_metars(client: &HttpClient, etag: Option) -> ApiResult { + let (remote_metars, etag) = Self::get_cached_remote_metars(client, etag) + .await + .unwrap_or_else(|err| { + log::warn!("Unable to get cached remote METAR data; {}", err); + (vec![], String::new()) + }); + for remote_metar in remote_metars.clone() { + remote_metar.insert().await?; + } + + Ok(etag) } pub async fn insert(&self) -> ApiResult<()> { @@ -1127,7 +1201,7 @@ impl Metar { self.icao, self.observation_time ); - let metar: MetarRow = self.to_db()?; + let metar: MetarRow = self.to_row()?; metar.insert().await?; Ok(()) } diff --git a/api/src/metars/routes.rs b/api/src/metars/routes.rs index 0fb9eba..9ece002 100644 --- a/api/src/metars/routes.rs +++ b/api/src/metars/routes.rs @@ -1,10 +1,12 @@ use crate::AppState; use crate::metars::Metar; -use actix_web::{HttpRequest, HttpResponse, get, web}; +use actix_web::{HttpRequest, HttpResponse, get, put, web}; use log::error; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use utoipa::{IntoParams, ToSchema}; +use utoipa_actix_web::scope; use utoipa_actix_web::service_config::ServiceConfig; +use crate::account::Auth; #[derive(Debug, Deserialize, ToSchema, IntoParams)] #[into_params(parameter_in = Query)] @@ -13,16 +15,16 @@ struct MetarQuery { } #[utoipa::path( - tag = "METARs", + tag = "metar", params( MetarQuery, ), responses( - (status = 200, description = "", body = [Metar]), + (status = 200, description = "Successful Response", body = [Metar]), ), )] -#[get("/metars")] -async fn find_all(data: web::Data, req: HttpRequest) -> HttpResponse { +#[get("")] +async fn find_all(req: HttpRequest) -> HttpResponse { let parameters = web::Query::::from_query(req.query_string()).unwrap(); let icao_option = ¶meters.icaos; if let None = icao_option { @@ -33,10 +35,9 @@ async fn find_all(data: web::Data, req: HttpRequest) -> HttpResponse { Some(i) => i, None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"), }; - let icaos: Vec = icao_string.split(',').map(|s| s.to_string()).collect(); + let icaos: Vec = icao_string.split(',').map(|s| s.to_uppercase()).collect(); - let client = &data.client; - let metars = match Metar::find_all_distinct(client, &icaos).await { + let metars = match Metar::get_all_distinct(&icaos).await { Ok(a) => a, Err(err) => { error!("{}", err); @@ -46,6 +47,75 @@ async fn find_all(data: web::Data, req: HttpRequest) -> HttpResponse { HttpResponse::Ok().json(metars) } -pub fn init_routes(config: &mut ServiceConfig) { - config.service(find_all); +#[utoipa::path( + tag = "metar", + params( + MetarQuery, + ), + responses( + (status = 200, description = "Successful Response", body = [Metar]), + (status = 401, description = "Unauthorized"), + ), + security( + ("session_auth" = []) + ) +)] +#[put("")] +async fn refresh_metars(data: web::Data, req: HttpRequest, _auth: Auth) -> HttpResponse { + let client = data.client.clone(); + let parameters = web::Query::::from_query(req.query_string()).unwrap(); + let icao_option = ¶meters.icaos; + if let None = icao_option { + let empty_metars: Vec = vec![]; + return HttpResponse::Ok().json(empty_metars); + } + let icao_string = match icao_option { + Some(i) => i, + None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"), + }; + let icaos: Vec = icao_string.split(',').map(|s| s.to_uppercase()).collect(); + + let metars = match Metar::get_or_update_metars(&client, &icaos).await { + Ok(a) => a, + Err(err) => { + error!("{}", err); + return err.to_http_response(); + } + }; + HttpResponse::Ok().json(metars) +} + +#[utoipa::path( + tag = "metar", + responses( + (status = 200, description = "Successful Response", body = Metar), + (status = 404, description = "Not Found"), + ), +)] +#[get("/{icao}")] +async fn find(icao: web::Path) -> HttpResponse { + let icao = vec![icao.to_uppercase()]; + let metar = match Metar::get_all_distinct(&icao).await { + Ok(metars) => { + if metars.len() == 1 { + metars[0].clone() + } else { + return HttpResponse::NotFound().finish() + } + }, + Err(err) => { + error!("{}", err); + return err.to_http_response(); + } + }; + HttpResponse::Ok().json(metar) +} + +pub fn init_routes(config: &mut ServiceConfig) { + config.service( + scope::scope("/metars") + .service(find_all) + .service(refresh_metars) + .service(find) + ); } diff --git a/api/src/scheduler.rs b/api/src/scheduler.rs index ef18a1f..d957f2d 100644 --- a/api/src/scheduler.rs +++ b/api/src/scheduler.rs @@ -1,74 +1,37 @@ -// use tokio::time::{sleep, Duration}; +use crate::http_client::HttpClient; +use crate::metars::Metar; +use chrono::{DateTime, Utc}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::time::interval; -// use crate::airports::{AirportDb, AirportFilter}; -// use crate::metars::Metar; +pub fn update_metars(client: Arc, seconds: u64) { + tokio::spawn(async move { + // Create interval ticker + let mut interval = interval(Duration::from_secs(seconds)); + let mut etag = None; -pub fn update_airports() { - // tokio::spawn(async { - // let mut airports: Vec = vec![]; - // let limit = 100; - // loop { - // log::debug!("METAR update start"); - // let total = match AirportDb::count(&AirportFilter::default()).await { - // Ok(t) => t, - // Err(err) => { - // log::warn!("{}", err); - // break; - // } - // }; - // if total != airports.len() as i64 { - // log::debug!("{} cached airports, expected {}", airports.len(), total); - // airports = vec![]; - // let pages = ((total as f32) / (if limit <= 0 { 1 } else { limit } as f32)).ceil() as i32; - // for page in 1..(pages + 1) { - // match AirportDb::find_all(&AirportFilter::default(), limit, page).await { - // Ok(mut a) => airports.append(&mut a), - // Err(err) => { - // log::warn!("{}", err); - // break; - // } - // } - // } - // } - // log::debug!("Updating {} airport METARS", airports.len()); - // - // let airport_icaos: Vec = airports.iter().map(|a| a.icao.to_string()).collect(); - // let mut peekable = airport_icaos.into_iter().peekable(); - // let mut observation_time = chrono::Utc::now().timestamp(); - // - // if peekable.peek().is_none() { - // log::debug!("No airports to update, sleeping for 1 hour"); - // sleep(Duration::from_secs(3600)).await; - // continue; - // } - // - // while peekable.peek().is_some() { - // let chunk: Vec = peekable.by_ref().take(limit as usize).collect(); - // let icao_string = chunk.join(","); - // log::warn!("Updating METARS for: {}", &icao_string); // TODO: back to trace after - // match Metar::find_all(&[&icao_string]).await { - // Ok(metars) => { - // // Find the oldest observation time - // for metar in metars { - // if metar.observation_time.timestamp() < observation_time { - // observation_time = metar.observation_time.timestamp(); - // } - // } - // } - // Err(err) => { - // log::warn!("{}", err); - // } - // } - // // Sleep for 100ms between chunks to avoid rate limiting - // sleep(Duration::from_millis(100)).await; - // } - // log::debug!("METAR update complete"); - // // Sleep until the earliest observation time is 1 hour old - // // Bounded by 1 and 3600 seconds - // let now = chrono::Utc::now().timestamp(); - // let sleep_time = std::cmp::min(std::cmp::max(1, now - (observation_time + 3600)), 3600); - // log::debug!("Next update in {} seconds", sleep_time); - // sleep(Duration::from_secs(sleep_time as u64)).await; - // } - // }); + loop { + interval.tick().await; + + // Record start times + let start_monotonic = Instant::now(); + let start_utc: DateTime = Utc::now(); + log::debug!("METAR update started at {}", start_utc); + + // Run the update + match Metar::update_metars(&client, etag.clone()).await { + Ok(new_etag) => etag = Some(new_etag), + Err(err) => log::error!("METAR update failed: {}", err) + } + + let elapsed = start_monotonic.elapsed(); + let next_utc = Utc::now() + chrono::Duration::from_std(Duration::from_secs(seconds)).unwrap(); + log::info!( + "METAR update finished in {:.2?}; next run at {}", + elapsed, + next_utc + ); + } + }); } diff --git a/api/src/smtp/mod.rs b/api/src/smtp/mod.rs index d7fb31e..2d9ad33 100644 --- a/api/src/smtp/mod.rs +++ b/api/src/smtp/mod.rs @@ -1,28 +1,36 @@ use crate::error::ApiResult; -use chrono::{Datelike, Utc}; use handlebars::Handlebars; use lettre::message::header::ContentType; use lettre::message::{Mailbox, MultiPart, SinglePart}; +use lettre::transport::smtp::AsyncSmtpTransportBuilder; use lettre::transport::smtp::authentication::Credentials; -use lettre::{Address, Message, SmtpTransport, Transport}; -use serde::Serialize; -use std::path::Path; +use lettre::{Address, AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; +use std::env; use std::sync::OnceLock; -use std::{env, fs}; +use std::time::Duration; -static MAILER: OnceLock = OnceLock::new(); +static MAILER: OnceLock> = OnceLock::new(); static FROM_ADDRESS: OnceLock = OnceLock::new(); static REGISTRY: OnceLock = OnceLock::new(); -fn mailer() -> &'static SmtpTransport { +fn mailer() -> &'static AsyncSmtpTransport { MAILER.get_or_init(|| { let server = env::var("SMTP_SERVER").expect("SMTP_SERVER missing"); let username = env::var("SMTP_USERNAME").expect("SMTP_USERNAME missing"); let password = env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD missing"); + let port = env::var("SMTP_PORT").expect("SMTP_PORT missing"); let creds = Credentials::new(username, password); - SmtpTransport::relay(&server) - .expect("invalid SMTP_SERVER") + let builder: AsyncSmtpTransportBuilder; + if server == "localhost" || server == "127.0.0.1" { + builder = AsyncSmtpTransport::::builder_dangerous(&server); + log::warn!("Using a local SMTP server: {}", server); + } else { + builder = AsyncSmtpTransport::::relay(&server).expect("invalid SMTP_SERVER"); + } + builder .credentials(creds) + .port(port.parse().expect("SMTP_PORT invalid")) + .timeout(Some(Duration::from_secs(10))) .build() }) } @@ -39,7 +47,7 @@ pub fn registry() -> &'static Handlebars<'static> { REGISTRY.get_or_init(|| Handlebars::new()) } -pub fn send_email(to: &str, subject: &str, header: String, html: String) -> ApiResult<()> { +pub async fn send_email(to: &str, subject: &str, header: String, html: String) -> ApiResult<()> { let to_address = to.parse::
()?; let to_mailbox = Mailbox::new(None, to_address); @@ -63,6 +71,6 @@ pub fn send_email(to: &str, subject: &str, header: String, html: String) -> ApiR )?; // Send the email - mailer().send(&email)?; + mailer().send(email).await?; Ok(()) } diff --git a/api/src/system/mod.rs b/api/src/system/mod.rs index 1a0b758..0575587 100644 --- a/api/src/system/mod.rs +++ b/api/src/system/mod.rs @@ -12,23 +12,16 @@ pub struct SystemInfo { } #[utoipa::path( - tag = "System", + tag = "system", responses( (status = 200, description = "Successful system info"), ) )] #[get("/info")] async fn info() -> HttpResponse { - let mut healthy = true; - let version = match env::var("CARGO_PKG_VERSION") { - Ok(v) => v, - Err(_) => { - healthy = false; - String::from("unknown") - } - }; - - let info = SystemInfo { version, healthy }; + let healthy = true; + let version = env!("CARGO_PKG_VERSION"); + let info = SystemInfo { version: version.to_string(), healthy }; HttpResponse::Ok().json(info) } diff --git a/api/src/users/model.rs b/api/src/users/model.rs index 0501d7f..f32e107 100644 --- a/api/src/users/model.rs +++ b/api/src/users/model.rs @@ -2,20 +2,34 @@ use crate::db; use crate::{account::hash, error::ApiResult}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[allow(unused_imports)] // Import is used in schema examples use serde_json::json; use sqlx::{Postgres, QueryBuilder}; use utoipa::ToSchema; -use uuid::Uuid; pub const ADMIN_ROLE: &str = "ADMIN"; pub const USER_ROLE: &str = "USER"; const TABLE_NAME: &str = "users"; #[derive(Debug, Deserialize, ToSchema)] +#[schema( + example = json!( + { + "email": "user", + "email": "user@example.com", + "password": "changeme", + "firstName": "firstname", + "lastName": "lastname" + } + ) +)] pub struct RegisterRequest { - pub email: String, + pub username: String, + pub email: Option, pub password: String, + #[serde(rename = "firstName")] pub first_name: String, + #[serde(rename = "lastName")] pub last_name: String, } @@ -23,8 +37,11 @@ impl RegisterRequest { pub fn to_user(self) -> ApiResult { let password_hash = hash(&self.password)?; Ok(User { - id: Uuid::new_v4(), - email: self.email.to_lowercase(), + username: self.username, + email: match self.email { + Some(email) => Some(email.to_lowercase()), + None => None, + }, email_verified: false, password_hash, role: USER_ROLE.to_string(), @@ -41,31 +58,34 @@ impl RegisterRequest { #[schema( example = json!( { - "email": "user@example.com", + "username": "admin", "password": "changeme" } ) )] pub struct LoginRequest { - pub email: String, + pub username: String, pub password: String, } #[derive(Debug, Serialize, ToSchema)] pub struct UserResponse { - pub id: Uuid, + pub username: String, pub role: String, + #[serde(rename = "firstName")] pub first_name: String, + #[serde(rename = "lastName")] pub last_name: String, #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, + #[serde(rename = "emailVerified")] pub email_verified: bool, } impl From for UserResponse { fn from(user: User) -> Self { UserResponse { - id: user.id, + username: user.username, email_verified: user.email_verified, role: user.role, first_name: user.first_name, @@ -87,7 +107,7 @@ pub struct UpdateUser { } impl UpdateUser { - pub async fn update(&self, id: &Uuid) -> ApiResult { + pub async fn update(&self, username: &str) -> ApiResult { let pool = db::pool(); let mut query_builder: QueryBuilder = @@ -143,8 +163,8 @@ impl UpdateUser { query_builder.push("updated_at = "); query_builder.push_bind(Utc::now()); - query_builder.push(" WHERE id = "); - query_builder.push_bind(id); + query_builder.push(" WHERE username = "); + query_builder.push_bind(username); query_builder.push(" RETURNING *"); let query = query_builder.build_query_as::(); @@ -156,8 +176,8 @@ impl UpdateUser { #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct User { - pub id: Uuid, - pub email: String, + pub username: String, + pub email: Option, pub email_verified: bool, pub password_hash: String, pub role: String, @@ -169,19 +189,19 @@ pub struct User { } impl User { - pub async fn select(id: &Uuid) -> Option { + pub async fn select(username: &str) -> Option { let pool = db::pool(); let user: Option = sqlx::query_as::<_, Self>(&format!( r#" - SELECT * FROM {} WHERE id = $1 + SELECT * FROM {} WHERE username = $1 "#, TABLE_NAME )) - .bind(id) + .bind(username) .fetch_optional(pool) .await .unwrap_or_else(|err| { - log::error!("Unable to find user by id '{}': {}", id, err); + log::error!("Unable to find user '{}': {}", username, err); None }); @@ -192,11 +212,11 @@ impl User { let pool = db::pool(); let user: Option = sqlx::query_as::<_, Self>(&format!( r#" - SELECT * FROM {} WHERE email = LOWER($1) + SELECT * FROM {} WHERE email = $1 "#, TABLE_NAME )) - .bind(email) + .bind(email.to_lowercase()) .fetch_optional(pool) .await .unwrap_or_else(|err| { @@ -207,6 +227,7 @@ impl User { user } + #[allow(dead_code)] pub async fn count() -> i64 { let pool = db::pool(); @@ -226,7 +247,7 @@ impl User { let user: User = sqlx::query_as::<_, Self>(&format!( r#" INSERT INTO {} ( - id, + username, email, email_verified, password_hash, @@ -242,7 +263,7 @@ impl User { "#, TABLE_NAME, )) - .bind(&self.id) + .bind(&self.username) .bind(&self.email) .bind(&self.email_verified) .bind(&self.password_hash) diff --git a/bruno/Users/Change Password.bru b/bruno/Account/Change Password.bru similarity index 81% rename from bruno/Users/Change Password.bru rename to bruno/Account/Change Password.bru index fa58270..46bfd1c 100644 --- a/bruno/Users/Change Password.bru +++ b/bruno/Account/Change Password.bru @@ -1,7 +1,7 @@ meta { name: Change Password type: http - seq: 4 + seq: 6 } put { @@ -11,7 +11,9 @@ put { } body:json { - "New Password" + { + "password": "New Password" + } } script:post-response { diff --git a/bruno/Account/Confirm Password Reset.bru b/bruno/Account/Confirm Password Reset.bru new file mode 100644 index 0000000..b0dc9d9 --- /dev/null +++ b/bruno/Account/Confirm Password Reset.bru @@ -0,0 +1,18 @@ +meta { + name: Confirm Password Reset + type: http + seq: 8 +} + +post { + url: {{API_URL}}/account/password/verify + body: json + auth: none +} + +body:json { + { + "token": "token", + "password": "New Password" + } +} diff --git a/bruno/Users/Get Profile.bru b/bruno/Account/Get Profile.bru similarity index 91% rename from bruno/Users/Get Profile.bru rename to bruno/Account/Get Profile.bru index 6c02c61..c5f1931 100644 --- a/bruno/Users/Get Profile.bru +++ b/bruno/Account/Get Profile.bru @@ -1,7 +1,7 @@ meta { name: Get Profile type: http - seq: 7 + seq: 10 } get { diff --git a/bruno/Users/Login.bru b/bruno/Account/Login.bru similarity index 78% rename from bruno/Users/Login.bru rename to bruno/Account/Login.bru index a7332e4..774955b 100644 --- a/bruno/Users/Login.bru +++ b/bruno/Account/Login.bru @@ -1,7 +1,7 @@ meta { name: Login type: http - seq: 2 + seq: 4 } post { @@ -12,7 +12,7 @@ post { body:json { { - "email": "admin@example.com", + "username": "user", "password": "changeme" } } diff --git a/bruno/Users/Logout.bru b/bruno/Account/Logout.bru similarity index 62% rename from bruno/Users/Logout.bru rename to bruno/Account/Logout.bru index 3b9b855..196fb5f 100644 --- a/bruno/Users/Logout.bru +++ b/bruno/Account/Logout.bru @@ -1,7 +1,7 @@ meta { name: Logout type: http - seq: 3 + seq: 5 } post { @@ -12,7 +12,7 @@ post { body:json { { - "email": "john.doe@gmail.com", - "password": "fake_password123" + "email": "user@gmail.com", + "password": "changeme" } } diff --git a/bruno/Users/Refresh Session.bru b/bruno/Account/Refresh Session.bru similarity index 92% rename from bruno/Users/Refresh Session.bru rename to bruno/Account/Refresh Session.bru index 23d9565..38b14ba 100644 --- a/bruno/Users/Refresh Session.bru +++ b/bruno/Account/Refresh Session.bru @@ -1,7 +1,7 @@ meta { name: Refresh Session type: http - seq: 6 + seq: 9 } get { diff --git a/bruno/Users/Register.bru b/bruno/Account/Register.bru similarity index 52% rename from bruno/Users/Register.bru rename to bruno/Account/Register.bru index c699448..92d7539 100644 --- a/bruno/Users/Register.bru +++ b/bruno/Account/Register.bru @@ -12,9 +12,10 @@ post { body:json { { - "email": "john.doe@gmail.com", - "password": "fake_password123", - "first_name": "John", - "last_name": "Doe" + "username": "user", + "email": "user@example.com", + "password": "changeme", + "firstName": "John", + "lastName": "Doe" } } diff --git a/bruno/Account/Resend Email Confirmation.bru b/bruno/Account/Resend Email Confirmation.bru new file mode 100644 index 0000000..dd913e6 --- /dev/null +++ b/bruno/Account/Resend Email Confirmation.bru @@ -0,0 +1,11 @@ +meta { + name: Resend Email Confirmation + type: http + seq: 2 +} + +post { + url: {{API_URL}}/account/register/resend + body: none + auth: none +} diff --git a/bruno/Users/Reset Password.bru b/bruno/Account/Reset Password.bru similarity index 58% rename from bruno/Users/Reset Password.bru rename to bruno/Account/Reset Password.bru index ca05566..fc64ea3 100644 --- a/bruno/Users/Reset Password.bru +++ b/bruno/Account/Reset Password.bru @@ -1,11 +1,17 @@ meta { name: Reset Password type: http - seq: 5 + seq: 7 } post { url: {{API_URL}}/account/password/reset - body: none + body: json auth: none } + +body:json { + { + "email": "user@example.com" + } +} diff --git a/bruno/Account/Verify Email Confirmation.bru b/bruno/Account/Verify Email Confirmation.bru new file mode 100644 index 0000000..0876848 --- /dev/null +++ b/bruno/Account/Verify Email Confirmation.bru @@ -0,0 +1,17 @@ +meta { + name: Verify Email Confirmation + type: http + seq: 3 +} + +post { + url: {{API_URL}}/account/register/verify + body: json + auth: none +} + +body:json { + { + "token": "token" + } +} diff --git a/bruno/Account/folder.bru b/bruno/Account/folder.bru new file mode 100644 index 0000000..fcbc1e8 --- /dev/null +++ b/bruno/Account/folder.bru @@ -0,0 +1,3 @@ +meta { + name: Account +} diff --git a/docker-compose.yml b/docker-compose.yml index a5e5746..b4f5a11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,8 +25,7 @@ services: volumes: - ./ssl:/etc/nginx/ssl/ networks: - - frontend - - backend + - default <<: *default_restart postgres: @@ -43,7 +42,7 @@ services: ports: - "${POSTGRES_PORT:-5432}:5432" networks: - - backend + - default profiles: - backend <<: *default_restart @@ -61,7 +60,7 @@ services: timeout: 5s retries: 3 networks: - - backend + - default profiles: - backend <<: *default_restart @@ -80,7 +79,7 @@ services: - "${MINIO_PORT:-9000}:9000" - "${MINIO_INTERNAL_PORT:-9001}:9001" networks: - - backend + - default profiles: - backend command: server --console-address ":9001" /data @@ -113,32 +112,50 @@ services: - redis - minio networks: - - frontend - - backend + - default profiles: - api <<: *default_restart - ui-dev: - image: gitea.bensherriff.com/bsherriff/aviation-ui:latest - container_name: aviation-ui-dev - build: - context: . - dockerfile: Dockerfile - env_file: *env + # Development Containers +# ui-dev: +# image: gitea.bensherriff.com/bsherriff/aviation-ui:latest +# container_name: aviation-ui-dev +# build: +# context: . +# dockerfile: Dockerfile +# env_file: *env +# environment: +# - VITE_NODE_ENV=${VITE_NODE_ENV:-development} +# ports: +# - "${UI_PORT:-3000}:3000" +# volumes: +# - ./ui/src:/app/src +# - ./ui/public:/app/public +# - ./ui/styles:/app/styles +# networks: +# - default +# profiles: +# - dev +# command: ["npm", "run", "dev"] +# <<: *default_restart + mailpit: + image: axllent/mailpit + container_name: mailpit environment: - - VITE_NODE_ENV=${VITE_NODE_ENV:-development} + MP_MAX_MESSAGES: 5000 + MP_DATABASE: /data/mailpit.db + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 ports: - - "${UI_PORT:-3000}:3000" + - "${MAILPIT_WEB_PORT:-8025}:8025" + - "${MAILPIT_SMTP_PORT:-1025}:1025" volumes: - - ./ui/src:/app/src - - ./ui/public:/app/public - - ./ui/styles:/app/styles + - mailpit:/data networks: - - frontend + - default profiles: - - frontend - command: ["npm", "run", "dev"] + - dev <<: *default_restart volumes: @@ -146,7 +163,7 @@ volumes: postgres_logs: redis: minio: + mailpit: networks: - frontend: - backend: + default: diff --git a/ui/src/App.tsx b/ui/src/App.tsx index c449f51..5300527 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -16,6 +16,7 @@ import { IconBuildingAirport, IconRadar } from '@tabler/icons-react'; import { GroupControl } from '@components/GroupControl.tsx'; import { AirportDrawer } from '@components/AirportDrawer'; import { LocateControl } from '@components/LocateControl.tsx'; +import { Footer } from '@components/Footer'; // Fix Leaflet's default icon path issues with Webpack // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error @@ -139,6 +140,7 @@ function App() { /> +