From e3d4f8eac80682a4bf328049daf34a7397220ad8 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Sun, 3 May 2026 22:23:11 +0200 Subject: [PATCH] authentication for the backend --- Cargo.lock | 1067 ++++++++++++++++- Cargo.toml | 5 + .../middleware/rate_limiting_middleware.rs | 5 +- src/controller/v1/mod.rs | 4 +- src/lib.rs | 10 + src/main.rs | 12 +- src/state.rs | 24 +- tests/auth/mod.rs | 3 + tests/auth/registration_login.rs | 34 + tests/auth/security.rs | 83 ++ tests/auth/session.rs | 62 + tests/auth_tests.rs | 2 + tests/basic_test.rs | 103 ++ tests/common/mod.rs | 48 + 14 files changed, 1415 insertions(+), 47 deletions(-) create mode 100644 src/lib.rs create mode 100644 tests/auth/mod.rs create mode 100644 tests/auth/registration_login.rs create mode 100644 tests/auth/security.rs create mode 100644 tests/auth/session.rs create mode 100644 tests/auth_tests.rs create mode 100644 tests/basic_test.rs create mode 100644 tests/common/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e540aab..b33f874 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,17 @@ dependencies = [ "password-hash", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -150,6 +161,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -186,6 +203,56 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bollard" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" +dependencies = [ + "base64", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-rustls", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.47.1-rc.27.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -277,6 +344,44 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[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" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -404,8 +509,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -422,13 +537,37 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] @@ -465,6 +604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -482,7 +622,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn", @@ -532,12 +672,38 @@ dependencies = [ "syn", ] +[[package]] +name = "docker_credential" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4564c274ebf369f501de192b02a0b81a5c4bda375abfe526aa70fc702fa6fa0" +dependencies = [ + "base64", + "serde", + "serde_json", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -606,6 +772,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -655,6 +830,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "ff" version = "0.13.1" @@ -671,6 +852,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -700,6 +892,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -709,6 +916,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -782,6 +1004,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -839,6 +1062,31 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -974,6 +1222,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -982,6 +1231,53 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -990,10 +1286,35 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", "pin-project-lite", "tokio", "tower-service", @@ -1138,6 +1459,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1150,6 +1482,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1171,6 +1519,8 @@ version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1231,7 +1581,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", "plain", "redox_syscall 0.7.4", @@ -1247,12 +1597,24 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1316,6 +1678,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1393,6 +1772,50 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl" +version = "0.10.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "p256" version = "0.13.2" @@ -1446,6 +1869,31 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -1573,6 +2021,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quote" version = "1.0.45" @@ -1635,13 +2099,22 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -1650,7 +2123,27 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags", + "bitflags 2.11.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1682,6 +2175,48 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -1705,10 +2240,13 @@ dependencies = [ "hex", "jsonwebtoken", "rand 0.10.1", + "reqwest", "serde", "serde_json", "sha2 0.11.0", "sqlx", + "testcontainers", + "testcontainers-modules", "thiserror", "time", "tokio", @@ -1723,6 +2261,20 @@ dependencies = [ "zxcvbn", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.10" @@ -1752,6 +2304,74 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[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.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1764,6 +2384,39 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1784,6 +2437,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1844,6 +2520,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1856,6 +2543,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2012,7 +2730,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap", + "indexmap 2.14.0", "log", "memchr", "once_cell", @@ -2075,7 +2793,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -2119,7 +2837,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -2199,6 +2917,29 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2227,6 +2968,9 @@ 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" @@ -2239,6 +2983,78 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "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.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "testcontainers" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a4f01f39bb10fc2a5ab23eb0d888b1e2bb168c157f61a1b98e6c501c639c74" +dependencies = [ + "async-trait", + "bollard", + "bollard-stubs", + "bytes", + "docker_credential", + "either", + "etcetera", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "tokio-stream", + "tokio-tar", + "tokio-util", + "url", +] + +[[package]] +name = "testcontainers-modules" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d43ed4e8f58424c3a2c6c56dbea6643c3c23e8666a34df13c54f0a184e6c707" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -2351,6 +3167,26 @@ dependencies = [ "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -2362,6 +3198,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tar" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.3" @@ -2400,11 +3264,14 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.11.1", "bytes", + "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -2522,6 +3389,12 @@ dependencies = [ "tracing-subscriber", ] +[[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.20.0" @@ -2561,6 +3434,12 @@ 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.8" @@ -2571,6 +3450,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -2609,6 +3489,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2652,6 +3541,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.118" @@ -2701,7 +3600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -2712,9 +3611,9 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -2738,6 +3637,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2779,6 +3700,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2803,7 +3735,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "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]] @@ -2821,13 +3762,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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", + "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]] @@ -2836,42 +3793,90 @@ 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_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_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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[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_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_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_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 = "wit-bindgen" version = "0.51.0" @@ -2906,7 +3911,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -2936,8 +3941,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", - "indexmap", + "bitflags 2.11.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -2956,7 +3961,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", @@ -2972,6 +3977,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 28d0b1b..26613d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,8 @@ tower = "0.5.3" futures-util = "0.3.32" dashmap = "6.1.0" zxcvbn = "3.1.1" + +[dev-dependencies] +testcontainers = "0.23.1" +testcontainers-modules = { version = "0.11.4", features = ["postgres"] } +reqwest = { version = "0.12", features = ["json", "cookies"] } diff --git a/src/controller/middleware/rate_limiting_middleware.rs b/src/controller/middleware/rate_limiting_middleware.rs index df316e8..dc88475 100644 --- a/src/controller/middleware/rate_limiting_middleware.rs +++ b/src/controller/middleware/rate_limiting_middleware.rs @@ -12,6 +12,7 @@ pub async fn rate_limiting_middleware( request: Request, next: Next, ) -> Response { + // 1. Identify client by IP (x-client-ip header or socket address) let client_ip = request .headers() .get("x-client-ip") @@ -25,14 +26,16 @@ pub async fn rate_limiting_middleware( .unwrap_or_else(|| "unknown".to_string()) }); + // 2. Retrieve or create a TokenBucket for this IP and try to drain 1 token let has_tokens = { let mut entry = state .rate_limit .entry(client_ip) .or_insert_with(crate::state::TokenBucket::new); - entry.value_mut().take() + entry.value_mut().try_drain() }; + // 3. If successful, proceed; else return 429 Too Many Requests if has_tokens { next.run(request).await } else { diff --git a/src/controller/v1/mod.rs b/src/controller/v1/mod.rs index 32a52d4..1740463 100644 --- a/src/controller/v1/mod.rs +++ b/src/controller/v1/mod.rs @@ -1,10 +1,10 @@ use axum::{ Router, - middleware::{from_fn, from_fn_with_state}, + middleware::from_fn_with_state, }; use crate::{ - controller::{middleware::auth_middleware::auth_middleware, v1::protected::protected_router}, + controller::middleware::auth_middleware::auth_middleware, state::AppState, }; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ceda08a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +pub mod config; +pub mod controller; +pub mod database; +pub mod db; +pub mod errors; +pub mod logging; +pub mod server; +pub mod service; +pub mod state; +pub mod utils; diff --git a/src/main.rs b/src/main.rs index 0d8bdcb..4f66363 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,4 @@ -mod config; -mod controller; -mod database; -mod db; -mod errors; -mod logging; -mod server; -mod service; -mod state; -mod utils; -use errors::MainError; +use rhythm_backend::{config, database, errors::MainError, logging, server}; #[tokio::main] async fn main() -> Result<(), MainError> { diff --git a/src/state.rs b/src/state.rs index ed9cf6d..8948aa6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,9 +3,6 @@ use sqlx::PgPool; use std::sync::Arc; use std::time::Instant; -const REFILL_RATE: f64 = 1.0; -const MAX_TOKENS: f64 = 10.0; - #[derive(Clone)] pub struct AppState { pub db: PgPool, @@ -18,21 +15,34 @@ pub struct TokenBucket { pub last_refill: Instant, } +// --- Rate Limiting Configuration --- +// Strategy: 5 requests per minute. +// This means 1 new token is mathematically generated every 12 seconds. +const REQUESTS_PER_MINUTE: f64 = 5.0; +// Maximum burst capacity. Users can make up to 5 rapid requests if their bucket is full. +const BUCKET_CAPACITY: f64 = 5.0; + impl TokenBucket { pub fn new() -> Self { Self { - tokens: MAX_TOKENS, + tokens: BUCKET_CAPACITY, last_refill: Instant::now(), } } - pub fn take(&mut self) -> bool { + /// Refills the bucket based on time passed since the last request. + /// Mathematically simulates a background process adding 1 token every (60/RPM) seconds. + fn refill(&mut self) { let now = Instant::now(); let elapsed = now.duration_since(self.last_refill).as_secs_f64(); + let tokens_per_second = REQUESTS_PER_MINUTE / 60.0; - self.tokens = (self.tokens + elapsed * REFILL_RATE).min(MAX_TOKENS); + self.tokens = (self.tokens + elapsed * tokens_per_second).min(BUCKET_CAPACITY); self.last_refill = now; - + } + /// Attempts to consume 1 token from the bucket. + pub fn try_drain(&mut self) -> bool { + self.refill(); if self.tokens >= 1.0 { self.tokens -= 1.0; true diff --git a/tests/auth/mod.rs b/tests/auth/mod.rs new file mode 100644 index 0000000..7043108 --- /dev/null +++ b/tests/auth/mod.rs @@ -0,0 +1,3 @@ +pub mod registration_login; +pub mod security; +pub mod session; diff --git a/tests/auth/registration_login.rs b/tests/auth/registration_login.rs new file mode 100644 index 0000000..f263088 --- /dev/null +++ b/tests/auth/registration_login.rs @@ -0,0 +1,34 @@ +use crate::common::{setup_app, spawn_server}; + +#[tokio::test] +async fn test_register_and_login() { + let (app, _db) = setup_app().await; + let (base_url, client) = spawn_server(app).await; + let email = format!("user_{}@test.com", uuid::Uuid::new_v4()); + + // Register + let resp = client + .post(format!("{}/api/v1/auth/register", base_url)) + .json(&serde_json::json!({"email": email, "password": "SuperSecureP@ssw0rd2024!"})) + .send().await.unwrap(); + + assert!(resp.status().is_success(), "Register failed: {}", resp.text().await.unwrap_or_default()); + let body: serde_json::Value = resp.json().await.unwrap(); + assert!(body["access_token"].is_string()); + + // Login success + let resp = client + .post(format!("{}/api/v1/auth/login", base_url)) + .json(&serde_json::json!({"email": email, "password": "SuperSecureP@ssw0rd2024!"})) + .send().await.unwrap(); + + assert!(resp.status().is_success()); + + // Login failure + let resp = client + .post(format!("{}/api/v1/auth/login", base_url)) + .json(&serde_json::json!({"email": email, "password": "WrongPassword"})) + .send().await.unwrap(); + + assert_eq!(resp.status(), 401); +} \ No newline at end of file diff --git a/tests/auth/security.rs b/tests/auth/security.rs new file mode 100644 index 0000000..c4bc42a --- /dev/null +++ b/tests/auth/security.rs @@ -0,0 +1,83 @@ +use crate::common::{setup_app, spawn_server}; +use std::time::{Duration, Instant}; + +#[tokio::test] +async fn test_rate_limiting_blocks_spam() { + let (app, _db) = setup_app().await; + let (base_url, client) = spawn_server(app).await; + + // Send 6 requests CONCURRENTLY so they arrive nearly simultaneously + // Capacity is 5, so exactly 5 should pass and 1 should fail. + let mut handles = Vec::new(); + for _ in 0..6 { + let c = client.clone(); + let url = format!("{}/api/v1/auth/login", base_url); + handles.push(tokio::spawn(async move { + c.post(&url) + .header("x-client-ip", "1.2.3.4") + .json(&serde_json::json!({"email": "a@test.com", "password": "b"})) + .send().await.unwrap() + .status().as_u16() + })); + } + + let statuses: Vec = futures_util::future::join_all(handles).await + .into_iter().map(|r| r.unwrap()).collect(); + + let successes = statuses.iter().filter(|&&s| s != 429).count(); + let blocked = statuses.iter().filter(|&&s| s == 429).count(); + + assert!(successes <= 5, "At most 5 rapid requests should be allowed, got {} passing", successes); + assert!(blocked >= 1, "At least 1 request should be rate-limited (429)"); + + // Different IP should still work + let ok = client + .post(format!("{}/api/v1/auth/login", base_url)) + .header("x-client-ip", "5.6.7.8") + .json(&serde_json::json!({"email": "a@test.com", "password": "b"})) + .send().await.unwrap(); + + assert_ne!(ok.status(), 429, "Different IP should not be rate-limited"); +} + +#[tokio::test] +async fn test_anti_enumeration_timing() { + let (app, _db) = setup_app().await; + let (base_url, client) = spawn_server(app).await; + + let start = Instant::now(); + let _ = client + .post(format!("{}/api/v1/auth/login", base_url)) + .header("x-client-ip", "1.1.1.1") // Different IP to avoid rate limits from previous requests in other tests if run concurrently + .json(&serde_json::json!({"email": "ghost_not_real@test.com", "password": "irrelevant"})) + .send().await.unwrap(); + let duration_nonexistent = start.elapsed(); + + let email = format!("timing_{}@test.com", uuid::Uuid::new_v4()); + let resp = client + .post(format!("{}/api/v1/auth/register", base_url)) + .header("x-client-ip", "2.2.2.2") + .json(&serde_json::json!({"email": email, "password": "SuperSecureP@ssw0rd2024!"})) + .send().await.unwrap(); + assert!(resp.status().is_success()); + + let start = Instant::now(); + let _ = client + .post(format!("{}/api/v1/auth/login", base_url)) + .header("x-client-ip", "3.3.3.3") + .json(&serde_json::json!({"email": email, "password": "WrongPassword123!"})) + .send().await.unwrap(); + let duration_existent = start.elapsed(); + + // Anti-enumeration middleware ensures BOTH take >= 150ms + assert!(duration_nonexistent >= Duration::from_millis(150), "Fast path should be padded to >= 150ms"); + assert!(duration_existent >= Duration::from_millis(150), "Slow path should be padded to >= 150ms"); + + let diff = if duration_nonexistent > duration_existent { + duration_nonexistent - duration_existent + } else { + duration_existent - duration_nonexistent + }; + assert!(diff < Duration::from_millis(300), + "Timing difference should be <300ms because both paths are padded by random delay: got {:?}", diff); +} \ No newline at end of file diff --git a/tests/auth/session.rs b/tests/auth/session.rs new file mode 100644 index 0000000..3ec97fb --- /dev/null +++ b/tests/auth/session.rs @@ -0,0 +1,62 @@ +use crate::common::{setup_app, spawn_server}; + +#[tokio::test] +async fn test_protected_route_requires_auth() { + let (app, _db) = setup_app().await; + let (base_url, client) = spawn_server(app).await; + + // No token → 401 + let resp = client + .get(format!("{}/api/v1/protected/ping", base_url)) + .send().await.unwrap(); + assert_eq!(resp.status(), 401, "Protected route should require auth"); + + // With token → 200 + let email = format!("protected_{}@test.com", uuid::Uuid::new_v4()); + let reg = client + .post(format!("{}/api/v1/auth/register", base_url)) + .json(&serde_json::json!({"email": email, "password": "SuperSecureP@ssw0rd2024!"})) + .send().await.unwrap(); + let token: serde_json::Value = reg.json().await.unwrap(); + + let resp = client + .get(format!("{}/api/v1/protected/ping", base_url)) + .bearer_auth(token["access_token"].as_str().unwrap()) + .send().await.unwrap(); + assert_eq!(resp.status(), 200, "Protected route should succeed with valid token"); +} + +#[tokio::test] +async fn test_refresh_and_logout_all() { + let (app, _db) = setup_app().await; + let (base_url, client) = spawn_server(app).await; + + // Register + login to get a valid session + let email = format!("refresh_{}@test.com", uuid::Uuid::new_v4()); + let reg = client + .post(format!("{}/api/v1/auth/register", base_url)) + .json(&serde_json::json!({"email": email, "password": "SuperSecureP@ssw0rd2024!"})) + .send().await.unwrap(); + let _token: serde_json::Value = reg.json().await.unwrap(); + + // Refresh should work + let refreshed = client + .post(format!("{}/api/v1/auth/refresh", base_url)) + .send().await.unwrap(); + assert!(refreshed.status().is_success(), "Refresh should succeed with cookie"); + let new_token: serde_json::Value = refreshed.json().await.unwrap(); + assert!(new_token["access_token"].is_string()); + + // Logout all + let resp = client + .post(format!("{}/api/v1/protected/auth/logout-all", base_url)) + .bearer_auth(new_token["access_token"].as_str().unwrap()) + .send().await.unwrap(); + assert!(resp.status().is_success(), "logout-all should succeed"); + + // After logout-all, refresh should fail + let fail = client + .post(format!("{}/api/v1/auth/refresh", base_url)) + .send().await.unwrap(); + assert_eq!(fail.status(), 401, "Refresh should fail after logout-all"); +} \ No newline at end of file diff --git a/tests/auth_tests.rs b/tests/auth_tests.rs new file mode 100644 index 0000000..bf59c83 --- /dev/null +++ b/tests/auth_tests.rs @@ -0,0 +1,2 @@ +mod common; +mod auth; diff --git a/tests/basic_test.rs b/tests/basic_test.rs new file mode 100644 index 0000000..ff2514f --- /dev/null +++ b/tests/basic_test.rs @@ -0,0 +1,103 @@ +use reqwest::Client; +use rhythm_backend::{ + controller, state::AppState, +}; +use axum::Router; +use sqlx::PgPool; +use std::sync::Arc; +use dashmap::DashMap; + +async fn setup_app() -> (Router, PgPool) { + let db_url = "postgres://user:password@localhost:5432/rhythm-dev?sslmode=disable"; + + let db = PgPool::connect(db_url) + .await + .expect("Failed to connect to Postgres at localhost:5432"); + + // Run migrations + sqlx::migrate!("./migrations") + .run(&db) + .await + .expect("Failed to run migrations"); + + let state = AppState { + db: db.clone(), + jwt_secret: "test-secret-key-12345678901234567890".to_string(), + rate_limit: Arc::new(DashMap::new()), + }; + + let app = controller::router(state.clone()).with_state(state); + + (app, db) +} + +#[tokio::test] +async fn test_register_and_login() { + let (app, _db) = setup_app().await; + + // Start the server on a random port + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + tokio::spawn(async move { + axum::serve(listener, app.into_make_service()).await.unwrap(); + }); + + // Give server a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + let base_url = format!("http://127.0.0.1:{}", port); + let client = Client::new(); + + // Use a unique email so we don't conflict with previous test runs + let unique_email = format!("testuser_{}@test.com", uuid::Uuid::new_v4()); + + // Test 1: Register + let reg_resp = client + .post(format!("{}/api/v1/auth/register", base_url)) + .json(&serde_json::json!({ + "email": unique_email, + "password": "SuperSecureP@ssw0rd2024!" + })) + .send() + .await + .unwrap(); + + println!("Register status: {}", reg_resp.status()); + assert!( + reg_resp.status().is_success(), + "Register should succeed: {}", + reg_resp.text().await.unwrap_or_default() + ); + + // Test 2: Login with correct password + let login_resp = client + .post(format!("{}/api/v1/auth/login", base_url)) + .json(&serde_json::json!({ + "email": unique_email, + "password": "SuperSecureP@ssw0rd2024!" + })) + .send() + .await + .unwrap(); + + println!("Login status: {}", login_resp.status()); + assert!(login_resp.status().is_success(), "Login should succeed with correct password: {}", login_resp.text().await.unwrap_or_default()); + + let body: serde_json::Value = login_resp.json().await.unwrap(); + assert!(body.get("access_token").is_some(), "Login response should have access_token"); + + // Test 3: Login with wrong password + let bad_login = client + .post(format!("{}/api/v1/auth/login", base_url)) + .json(&serde_json::json!({ + "email": unique_email, + "password": "WrongPassword1!" + })) + .send() + .await + .unwrap(); + + println!("Bad login status: {}", bad_login.status()); + assert_eq!(bad_login.status(), 401, "Login with wrong password should return 401"); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..4ffedf5 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,48 @@ +use reqwest::Client; +use std::time::Duration; +use rhythm_backend::{controller, state::AppState}; +use axum::Router; +use sqlx::PgPool; +use std::sync::Arc; +use dashmap::DashMap; + +pub async fn setup_app() -> (Router, PgPool) { + let db_url = "postgres://user:password@localhost:5432/rhythm-dev?sslmode=disable"; + + let db = PgPool::connect(db_url) + .await + .expect("Failed to connect to Postgres at localhost:5432"); + + sqlx::migrate!("./migrations") + .run(&db) + .await + .expect("Failed to run migrations"); + + let state = AppState { + db: db.clone(), + jwt_secret: "test-secret-key-12345678901234567890".to_string(), + rate_limit: Arc::new(DashMap::new()), + }; + + let app = controller::router(state.clone()).with_state(state); + (app, db) +} + +pub async fn spawn_server(app: axum::Router) -> (String, Client) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + tokio::spawn(async move { + axum::serve(listener, app.into_make_service()).await.unwrap(); + }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + let base_url = format!("http://127.0.0.1:{}", port); + let client = Client::builder() + .cookie_store(true) + .build() + .unwrap(); + + (base_url, client) +}