diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 60a5c97..3e3319a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -19,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -127,7 +118,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", "synstructure", ] @@ -139,7 +130,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -150,7 +141,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -191,7 +182,7 @@ dependencies = [ "attohttpc", "home", "log", - "quick-xml 0.38.3", + "quick-xml", "rust-ini", "serde", "thiserror 2.0.17", @@ -211,16 +202,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", - "libloading", ] [[package]] @@ -234,13 +224,13 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.9" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "async-trait", "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -254,14 +244,13 @@ dependencies = [ "multer", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -269,19 +258,17 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -290,42 +277,31 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.6" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +checksum = "5136e6c5e7e7978fe23e9876fb924af2c0f84c72127ac6ac17e7c46f457d362c" dependencies = [ "axum", "axum-core", "bytes", - "fastrand", + "futures-core", "futures-util", "headers", "http", "http-body", "http-body-util", "mime", - "multer", "pin-project-lite", - "serde", - "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] -name = "backtrace" -version = "0.3.76" +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" @@ -365,7 +341,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -373,14 +349,14 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake2" @@ -441,9 +417,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.40" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "jobserver", @@ -462,9 +438,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -494,7 +470,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -528,7 +504,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -585,6 +561,12 @@ dependencies = [ "web-sys", ] +[[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" @@ -645,6 +627,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -655,6 +649,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "darling" version = "0.21.3" @@ -676,7 +697,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -687,7 +708,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -696,6 +717,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "der-parser" version = "9.0.0" @@ -712,9 +744,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", "serde_core", @@ -722,9 +754,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8496eeb328dce26ee9d9b73275d396d9bddb433fa30106cf6056dd8c3c2764c" +checksum = "5e7624a3bb9fffd82fff016be9a7f163d20e5a89eb8d28f9daaa6b30fff37500" dependencies = [ "bitflags", "byteorder", @@ -740,15 +772,15 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09af0e983035368439f1383011cd87c46f41da81d0f21dc3727e2857d5a43c8e" +checksum = "9daac6489a36e42570da165a10c424f3edcefdff70c5fd55e1847c23f3dd7562" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -768,7 +800,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" dependencies = [ - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -778,6 +810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -790,7 +823,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -825,7 +858,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -834,12 +867,71 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -890,16 +982,32 @@ dependencies = [ ] [[package]] -name = "find-msvc-tools" -version = "0.1.3" +name = "ff" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -970,7 +1078,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -1004,12 +1112,13 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1021,36 +1130,41 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "half" version = "2.7.1" @@ -1110,6 +1224,15 @@ 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" @@ -1279,9 +1402,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1292,9 +1415,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1305,11 +1428,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1320,42 +1442,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1418,9 +1536,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -1428,17 +1546,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -1447,9 +1554,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -1470,6 +1577,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1482,15 +1598,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -1498,16 +1614,24 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.1" +version = "10.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" dependencies = [ "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.16", + "hmac", "js-sys", + "p256", + "p384", "pem", - "ring", + "rand 0.8.5", + "rsa", "serde", "serde_json", + "sha2", + "signature", "simple_asn1", ] @@ -1516,23 +1640,42 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.5", + "windows-link 0.2.1", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1541,9 +1684,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -1577,9 +1720,9 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "maybe-async" @@ -1589,7 +1732,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -1674,20 +1817,20 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "moxcms" -version = "0.7.6" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc7d85f3d741164e8972ad355e26ac6e51b20fcae5f911c7da8f2d8bbbb3f33" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" dependencies = [ "num-traits", "pxfm", @@ -1748,11 +1891,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1765,6 +1908,23 @@ dependencies = [ "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" @@ -1780,6 +1940,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1787,6 +1958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1808,15 +1980,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -1840,9 +2003,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.74" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", @@ -1861,7 +2024,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -1872,9 +2035,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -1901,6 +2064,30 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "papercrate" version = "0.1.0" @@ -1910,7 +2097,7 @@ dependencies = [ "async-trait", "axum", "axum-extra", - "base64 0.21.7", + "base64 0.22.1", "bytes", "chrono", "clap", @@ -1928,8 +2115,8 @@ dependencies = [ "once_cell", "pdfium-render", "percent-encoding", - "quick-xml 0.32.0", - "rand 0.8.5", + "quick-xml", + "rand 0.9.2", "reqwest", "rust-s3", "serde", @@ -1939,9 +2126,9 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.17", "tokio", - "tower 0.4.13", + "tower", "tower-http", "tracing", "tracing-subscriber", @@ -1994,9 +2181,9 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pdfium-render" -version = "0.8.35" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8fa6e8ceaad45c9fed63db09045c45f2cccd4a8b92a826bd3f8e901a98b14c6" +checksum = "2c8bfacaecc7ff9bcc2f23fed64336830f823b13548a20083f3d0b0164537d2c" dependencies = [ "bitflags", "bytemuck", @@ -2005,9 +2192,9 @@ dependencies = [ "console_error_panic_hook", "console_log", "image", - "itertools", + "itertools 0.14.0", "js-sys", - "libloading", + "libloading 0.9.0", "log", "maybe-owned", "once_cell", @@ -2020,12 +2207,21 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", ] [[package]] @@ -2034,26 +2230,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2072,6 +2248,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad78bf43dcf80e8f950c92b84f938a0fc7590b7f6866fbcbeca781609c115590" +[[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" @@ -2093,9 +2290,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -2117,9 +2314,9 @@ dependencies = [ [[package]] name = "pq-sys" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "412a4cb9e93795c0594dab7c1c4ec1c8b42b514e1baf6e9f63d14aa376e5bd04" +checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61" dependencies = [ "libc", "pkg-config", @@ -2133,7 +2330,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.109", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", ] [[package]] @@ -2162,9 +2368,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -2184,15 +2390,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.38.3" @@ -2230,7 +2427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -2260,9 +2457,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2340,7 +2537,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -2354,9 +2551,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -2366,9 +2563,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -2377,15 +2574,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", @@ -2414,7 +2611,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tokio-util", - "tower 0.5.2", + "tower", "tower-http", "tower-service", "url", @@ -2425,6 +2622,16 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2439,6 +2646,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -2470,7 +2697,7 @@ dependencies = [ "md5", "minidom", "percent-encoding", - "quick-xml 0.38.3", + "quick-xml", "reqwest", "serde", "serde_derive", @@ -2484,18 +2711,21 @@ dependencies = [ "url", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -2520,9 +2750,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", @@ -2536,9 +2766,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -2546,9 +2776,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring", @@ -2611,6 +2841,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2634,6 +2878,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2703,7 +2953,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -2732,9 +2982,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -2797,6 +3047,16 @@ 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" @@ -2829,12 +3089,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2844,10 +3104,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -2879,9 +3149,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", @@ -2905,7 +3175,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -2929,7 +3199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2961,7 +3231,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -2972,7 +3242,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -3026,9 +3296,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -3051,33 +3321,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -3113,9 +3380,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -3126,9 +3393,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "serde_core", "serde_spanned", @@ -3139,38 +3406,22 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -3200,7 +3451,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -3238,7 +3489,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -3300,9 +3551,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "untrusted" @@ -3364,7 +3615,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", "uuid", ] @@ -3374,7 +3625,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", @@ -3422,15 +3673,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -3442,9 +3684,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -3453,25 +3695,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -3482,9 +3710,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3492,22 +3720,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.109", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -3527,9 +3755,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -3615,9 +3843,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -3711,7 +3939,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -3722,7 +3950,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -3792,15 +4020,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -3971,9 +4190,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "x509-parser" @@ -3994,11 +4213,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -4006,13 +4224,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", "synstructure", ] @@ -4033,7 +4251,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] @@ -4053,7 +4271,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", "synstructure", ] @@ -4065,9 +4283,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -4076,9 +4294,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -4087,13 +4305,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.109", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index ffa1978..dddccc8 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -5,14 +5,14 @@ edition = "2021" [dependencies] # Web framework -axum = { version = "0.7", features = ["multipart"] } -tokio = { version = "1", features = ["full"] } -tower = { version = "0.4", features = ["make", "util"] } +axum = { version = "0.8", features = ["multipart"] } +tokio = { version = "1.48", features = ["full"] } +tower = { version = "0.5", features = ["make", "util"] } tower-http = { version = "0.6", features = ["cors", "trace"] } -axum-extra = { version = "0.9", features = ["typed-header"] } +axum-extra = { version = "0.12", features = ["typed-header"] } # Database -diesel = { version = "2.1", features = ["postgres", "uuid", "chrono", "serde_json", "r2d2"] } +diesel = { version = "2.3.3", features = ["postgres", "uuid", "chrono", "serde_json", "r2d2"] } diesel_migrations = "2.1" uuid = { version = "1.6", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } @@ -35,13 +35,13 @@ hex = "0.4" bytes = "1.5" async-trait = "0.1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } -pdfium-render = "0.8" +pdfium-render = "0.8.36" mime_guess = "2.0" tempfile = "3.10" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } +reqwest = { version = "0.12.24", default-features = false, features = ["json", "rustls-tls", "stream"] } percent-encoding = "2.3" -base64 = "0.21" -quick-xml = "0.32" +base64 = "0.22" +quick-xml = "0.38" futures-util = "0.3" url = "2.5" once_cell = "1.19" @@ -49,23 +49,23 @@ utoipa = { version = "4.2", default-features = false, features = ["chrono", "uui clap = { version = "4.5", features = ["derive"] } # Error handling -thiserror = "1.0" +thiserror = "2.0" anyhow = "1.0" # Authentication & security argon2 = "0.5" -jsonwebtoken = "9" +jsonwebtoken = { version = "10", features = ["rust_crypto"] } webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-credential-internals"] } serde_bytes = "0.11" serde_cbor_2 = "0.13" # Misc -rand = "0.8" +rand = "0.9" +hyper = "1.2" +http-body-util = "0.1" [dev-dependencies] once_cell = "1.19" -hyper = "1.2" -http-body-util = "0.1" webauthn-rs-core = "0.5" [[bin]] diff --git a/backend/src/auth/api_tokens.rs b/backend/src/auth/api_tokens.rs index 7b7fa28..5a75704 100644 --- a/backend/src/auth/api_tokens.rs +++ b/backend/src/auth/api_tokens.rs @@ -1,11 +1,10 @@ use argon2::{ - password_hash::{PasswordHasher, SaltString}, + password_hash::{rand_core::OsRng as PasswordHashOsRng, PasswordHasher, SaltString}, Argon2, }; use chrono::{NaiveDateTime, Utc}; use diesel::prelude::*; -use rand::rngs::OsRng; -use rand::RngCore; +use rand::{rngs::OsRng, TryRngCore}; use uuid::Uuid; use crate::{ @@ -264,7 +263,8 @@ fn generate_secret() -> Result { } fn hash_secret(secret: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); + let mut salt_rng = PasswordHashOsRng; + let salt = SaltString::generate(&mut salt_rng); let hash = Argon2::default() .hash_password(secret.as_bytes(), &salt) .map_err(|err| { diff --git a/backend/src/auth/jwt.rs b/backend/src/auth/jwt.rs index 56aec99..75afd64 100644 --- a/backend/src/auth/jwt.rs +++ b/backend/src/auth/jwt.rs @@ -2,11 +2,12 @@ use anyhow::Result; use chrono::{Duration, Utc}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use uuid::Uuid; use crate::config::AppConfig; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema)] #[serde(rename_all = "snake_case")] pub enum PrincipalKind { UserSession, diff --git a/backend/src/auth/mod.rs b/backend/src/auth/mod.rs index 8cb2140..cb1bafe 100644 --- a/backend/src/auth/mod.rs +++ b/backend/src/auth/mod.rs @@ -7,7 +7,7 @@ pub mod password; use std::sync::{Arc, Mutex}; -use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; +use axum::{extract::FromRequestParts, http::request::Parts}; use axum_extra::headers::{authorization::Bearer, Authorization}; use axum_extra::TypedHeader; use serde::{Deserialize, Serialize}; @@ -52,56 +52,59 @@ pub struct AuthenticatedUser { pub capabilities: Vec, } -#[async_trait] impl FromRequestParts for AuthenticatedUser { type Rejection = AppError; - async fn from_request_parts( - parts: &mut Parts, + #[allow(refining_impl_trait)] + fn from_request_parts<'a>( + parts: &'a mut Parts, state: &AppState, - ) -> Result { - if let Some(user) = parts.extensions.get::() { - return Ok(user.clone()); - } + ) -> impl std::future::Future> + Send + 'a { + let state = state.clone(); + async move { + if let Some(user) = parts.extensions.get::() { + return Ok(user.clone()); + } - let TypedHeader(Authorization(bearer)) = - TypedHeader::>::from_request_parts(parts, state) - .await + let TypedHeader(Authorization(bearer)) = + TypedHeader::>::from_request_parts(parts, &state) + .await + .map_err(|_| AppError::unauthorized())?; + + let claims = state + .jwt + .verify_token(bearer.token()) .map_err(|_| AppError::unauthorized())?; - let claims = state - .jwt - .verify_token(bearer.token()) - .map_err(|_| AppError::unauthorized())?; + let mut tenant_conn = state.db_for_tenant(claims.tenant_id)?; + let capability_set = load_capability_set(&mut tenant_conn, claims.capability_set_id) + .map_err(|_| AppError::unauthorized())?; - let mut tenant_conn = state.db_for_tenant(claims.tenant_id)?; - let capability_set = load_capability_set(&mut tenant_conn, claims.capability_set_id) - .map_err(|_| AppError::unauthorized())?; + if capability_set.cap_version != claims.cap_version { + return Err(AppError::unauthorized()); + } - if capability_set.cap_version != claims.cap_version { - return Err(AppError::unauthorized()); + let capabilities = load_capabilities_for_set(&mut tenant_conn, capability_set.id) + .map_err(|_| AppError::unauthorized())?; + + let user = AuthenticatedUser { + user_id: claims.sub, + username: claims.username, + tenant_id: claims.tenant_id, + principal_kind: claims.principal_kind, + principal_id: claims.principal_id, + capability_set_id: claims.capability_set_id, + cap_version: claims.cap_version, + capabilities, + }; + + parts.extensions.insert(user.clone()); + parts + .extensions + .insert(TenantConnectionHolder::new(tenant_conn)); + + Ok(user) } - - let capabilities = load_capabilities_for_set(&mut tenant_conn, capability_set.id) - .map_err(|_| AppError::unauthorized())?; - - let user = AuthenticatedUser { - user_id: claims.sub, - username: claims.username, - tenant_id: claims.tenant_id, - principal_kind: claims.principal_kind, - principal_id: claims.principal_id, - capability_set_id: claims.capability_set_id, - cap_version: claims.cap_version, - capabilities, - }; - - parts.extensions.insert(user.clone()); - parts - .extensions - .insert(TenantConnectionHolder::new(tenant_conn)); - - Ok(user) } } @@ -118,29 +121,32 @@ impl TenantScopedConn { } } -#[async_trait] impl FromRequestParts for TenantScopedConn { type Rejection = AppError; - async fn from_request_parts( - parts: &mut Parts, + #[allow(refining_impl_trait)] + fn from_request_parts<'a>( + parts: &'a mut Parts, state: &AppState, - ) -> Result { - let user = AuthenticatedUser::from_request_parts(parts, state).await?; - let tenant_id = user.tenant_id; - let conn = if let Some(holder) = parts.extensions.remove::() { - holder - .into_conn() - .ok_or_else(|| AppError::internal("tenant connection unavailable"))? - } else { - state.db_for_tenant(tenant_id)? - }; + ) -> impl std::future::Future> + Send + 'a { + let state = state.clone(); + async move { + let user = AuthenticatedUser::from_request_parts(parts, &state).await?; + let tenant_id = user.tenant_id; + let conn = if let Some(holder) = parts.extensions.remove::() { + holder + .into_conn() + .ok_or_else(|| AppError::internal("tenant connection unavailable"))? + } else { + state.db_for_tenant(tenant_id)? + }; - Ok(Self { - conn, - tenant_id, - user_id: user.user_id, - user, - }) + Ok(Self { + conn, + tenant_id, + user_id: user.user_id, + user, + }) + } } } diff --git a/backend/src/auth/password.rs b/backend/src/auth/password.rs index ff7489f..a79ceed 100644 --- a/backend/src/auth/password.rs +++ b/backend/src/auth/password.rs @@ -1,9 +1,11 @@ use anyhow::{anyhow, Result}; use argon2::{ - password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + password_hash::{ + rand_core::OsRng as PasswordHashOsRng, PasswordHash, PasswordHasher, PasswordVerifier, + SaltString, + }, Argon2, }; -use rand::rngs::OsRng; pub fn verify_password(password: &str, password_hash: &str) -> Result { let parsed_hash = PasswordHash::new(password_hash).map_err(|err| anyhow!(err))?; @@ -13,7 +15,8 @@ pub fn verify_password(password: &str, password_hash: &str) -> Result { } pub fn hash_password(password: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); + let mut rng = PasswordHashOsRng; + let salt = SaltString::generate(&mut rng); let hash = Argon2::default() .hash_password(password.as_bytes(), &salt) .map_err(|err| anyhow!(err))?; diff --git a/backend/src/bin/admin.rs b/backend/src/bin/admin.rs index a17dd1d..9945f49 100644 --- a/backend/src/bin/admin.rs +++ b/backend/src/bin/admin.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, bail, Context, Result}; use chrono::{Duration as ChronoDuration, Utc}; use clap::{Parser, Subcommand, ValueEnum}; use diesel::{dsl::exists, prelude::*, select}; -use rand::{rngs::OsRng, RngCore}; +use rand::{rngs::OsRng, TryRngCore}; use reqwest::{Client, Method, StatusCode}; use sha2::{Digest, Sha256}; use uuid::Uuid; @@ -323,7 +323,9 @@ fn create_magic_token( fn generate_random_token() -> String { let mut bytes = [0u8; 32]; - OsRng.fill_bytes(&mut bytes); + OsRng + .try_fill_bytes(&mut bytes) + .expect("failed to read random bytes"); hex::encode(bytes) } diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 7f2d135..10ea765 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -17,3 +17,4 @@ pub mod tenants; pub mod utils; pub mod workers; pub use workers::{default_handlers, Worker}; +pub mod test_support; diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index d1f9b4d..104fefa 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -785,7 +785,7 @@ pub async fn delete_document( patch, path = "/api/documents/{id}", params(("id" = Uuid, Path, description = "Document ID")), - request_body = UpdateDocumentRequest, + request_body = crate::services::documents::UpdateDocumentRequest, responses((status = 200, description = "Updated document", body = DocumentDetailResponse)), tag = "Documents" )] diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index b79bb48..efad73e 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -125,92 +125,92 @@ pub fn create_router(state: AppState) -> Router<()> { ])), ) .route( - "/:id", + "/{id}", get(documents::get_document).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsRead, ])), ) .route( - "/:id/trash", + "/{id}/trash", post(documents::trash_document).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsWrite, ])), ) .route( - "/:id", + "/{id}", delete(documents::delete_document).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsWrite, ])), ) .route( - "/:id", + "/{id}", patch(documents::update_document).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsEdit, ])), ) .route( - "/:id/assets", + "/{id}/assets", get(documents::list_document_assets).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsRead, ])), ) .route( - "/:id/assets", + "/{id}/assets", post(documents::request_document_assets).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsWrite, ])), ) .route( - "/:id/folder", + "/{id}/folder", patch(documents::move_document).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsEdit, ])), ) .route( - "/:id/versions", + "/{id}/versions", get(documents::list_document_versions).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsRead, ])), ) .route( - "/:id/versions/:version_id", + "/{id}/versions/{version_id}", get(documents::get_document_version).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsRead, ])), ) .route( - "/:id/restore", + "/{id}/restore", post(documents::restore_document).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsEdit, ])), ) .route( - "/:id/tags", + "/{id}/tags", post(documents::assign_tags).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsEdit, ])), ) .route( - "/:id/tags/:tag_id", + "/{id}/tags/{tag_id}", delete(documents::remove_tag).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsEdit, ])), ) .route( - "/:id/correspondents", + "/{id}/correspondents", post(documents::assign_correspondents).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsEdit, ])), ) .route( - "/:id/correspondents/:correspondent_id", + "/{id}/correspondents/{correspondent_id}", delete(documents::remove_correspondent).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsEdit, ])), ); let download_routes = - Router::new().route("/download/:token", get(documents::download_with_token)); + Router::new().route("/download/{token}", get(documents::download_with_token)); let folders_routes = Router::new() .route( @@ -229,22 +229,22 @@ pub fn create_router(state: AppState) -> Router<()> { .layer(RequireCapabilitiesLayer::all([ApiCapability::FoldersRead])), ) .route( - "/:id", + "/{id}", get(folders::get_folder) .layer(RequireCapabilitiesLayer::all([ApiCapability::FoldersRead])), ) .route( - "/:id", + "/{id}", delete(folders::delete_folder) .layer(RequireCapabilitiesLayer::all([ApiCapability::FoldersWrite])), ) .route( - "/:id", + "/{id}", patch(folders::update_folder) .layer(RequireCapabilitiesLayer::all([ApiCapability::FoldersEdit])), ) .route( - "/:id/contents", + "/{id}/contents", get(folders::list_folder_contents) .layer(RequireCapabilitiesLayer::all([ApiCapability::FoldersRead])), ); @@ -259,11 +259,11 @@ pub fn create_router(state: AppState) -> Router<()> { post(tags::create_tag).layer(RequireCapabilitiesLayer::all([ApiCapability::TagsWrite])), ) .route( - "/:id", + "/{id}", patch(tags::update_tag).layer(RequireCapabilitiesLayer::all([ApiCapability::TagsEdit])), ) .route( - "/:id", + "/{id}", delete(tags::delete_tag) .layer(RequireCapabilitiesLayer::all([ApiCapability::TagsWrite])), ); @@ -282,13 +282,13 @@ pub fn create_router(state: AppState) -> Router<()> { ])), ) .route( - "/:id", + "/{id}", patch(correspondents::update_correspondent).layer(RequireCapabilitiesLayer::all([ ApiCapability::CorrespondentsEdit, ])), ) .route( - "/:id", + "/{id}", delete(correspondents::delete_correspondent).layer(RequireCapabilitiesLayer::all([ ApiCapability::CorrespondentsWrite, ])), @@ -306,12 +306,12 @@ pub fn create_router(state: AppState) -> Router<()> { .layer(RequireCapabilitiesLayer::all([ApiCapability::ProfileWrite])), ) .route( - "/api-tokens/:id/regenerate", + "/api-tokens/{id}/regenerate", post(profile::regenerate_api_token) .layer(RequireCapabilitiesLayer::all([ApiCapability::ProfileWrite])), ) .route( - "/api-tokens/:id", + "/api-tokens/{id}", delete(profile::delete_api_token) .layer(RequireCapabilitiesLayer::all([ApiCapability::ProfileWrite])), ) @@ -321,7 +321,7 @@ pub fn create_router(state: AppState) -> Router<()> { .layer(RequireCapabilitiesLayer::all([ApiCapability::ProfileRead])), ) .route( - "/passkeys/:id", + "/passkeys/{id}", delete(profile::delete_passkey) .layer(RequireCapabilitiesLayer::all([ApiCapability::ProfileWrite])), ); @@ -340,19 +340,19 @@ pub fn create_router(state: AppState) -> Router<()> { ])), ) .route( - "/:id", + "/{id}", get(capability_sets::get_capability_set).layer(RequireCapabilitiesLayer::all([ ApiCapability::CapabilitySetsRead, ])), ) .route( - "/:id", + "/{id}", patch(capability_sets::update_capability_set).layer(RequireCapabilitiesLayer::all([ ApiCapability::CapabilitySetsWrite, ])), ) .route( - "/:id", + "/{id}", delete(capability_sets::delete_capability_set).layer(RequireCapabilitiesLayer::all([ ApiCapability::CapabilitySetsWrite, ])), @@ -367,7 +367,7 @@ pub fn create_router(state: AppState) -> Router<()> { let protected_state = state.clone(); let assets_routes = Router::new().route( - "/:asset_id", + "/{asset_id}", get(documents::get_document_asset).layer(RequireCapabilitiesLayer::all([ ApiCapability::DocumentsRead, ])), diff --git a/backend/src/services/auth.rs b/backend/src/services/auth.rs index c8f55d0..dc61d32 100644 --- a/backend/src/services/auth.rs +++ b/backend/src/services/auth.rs @@ -2,7 +2,7 @@ use axum::http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use chrono::{Duration as ChronoDuration, TimeZone, Utc}; use diesel::{pg::PgConnection, prelude::*, Connection, OptionalExtension}; -use rand::{rngs::OsRng, RngCore}; +use rand::{rngs::OsRng, TryRngCore}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use utoipa::ToSchema; @@ -781,7 +781,9 @@ fn hash_magic_token(token: &str) -> String { fn generate_session_token() -> String { let mut bytes = [0u8; 32]; - OsRng.fill_bytes(&mut bytes); + OsRng + .try_fill_bytes(&mut bytes) + .expect("failed to read random bytes"); hex::encode(bytes) } diff --git a/backend/src/test_support/mod.rs b/backend/src/test_support/mod.rs new file mode 100644 index 0000000..8b60d3e --- /dev/null +++ b/backend/src/test_support/mod.rs @@ -0,0 +1,891 @@ +use std::collections::HashMap; +use std::env; +use std::sync::Arc; +use std::time::Duration; + +use crate::auth::capability_sets::{ + ensure_capability_set, owner_capabilities, readonly_capabilities, user_capabilities, + webdav_capabilities, +}; +use crate::auth::jwt::{AccessTokenContext, JwtService, PrincipalKind}; +use crate::config::AppConfig; +use crate::db::{self, PgPool}; +use crate::models::{ + Job, NewUser, NewUserMembership, NewUserPasskey, NewUserSession, Tenant, TenantStatus, User, + UserMembership, +}; +use crate::routes; +use crate::schema::user_sessions::dsl as session_dsl; +use crate::state::AppState; +use crate::storage::ObjectStorage; +use anyhow::{anyhow, ensure, Context, Result}; +use async_trait::async_trait; +use axum::body::Body; +use axum::http::{header, Method, Request}; +use axum::Router; +use chrono::{Duration as ChronoDuration, Utc}; +use diesel::connection::SimpleConnection; +use diesel::prelude::*; +use diesel::OptionalExtension; +use diesel::PgConnection; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use http_body_util::BodyExt; +use once_cell::sync::Lazy; +use rand::{rngs::OsRng, TryRngCore}; +use serde::Serialize; +use serde_json::{self, json}; +use sha2::{Digest, Sha256}; +use tokio::sync::Mutex; +use tower::util::ServiceExt; +use uuid::Uuid; + +const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); +const RESET_DATABASE_SQL: &str = "DROP SCHEMA IF EXISTS tenant CASCADE;\n\ + DROP SCHEMA IF EXISTS shared CASCADE;\n\ + DROP SCHEMA IF EXISTS public CASCADE;\n\ + CREATE SCHEMA public;\n\ + GRANT ALL ON SCHEMA public TO public;"; + +static DB_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); + +const TEST_TENANT_NAME: &str = "test_tenant"; + +#[derive(Clone, Copy, Debug)] +pub enum TestUserRole { + Owner, + Member, + WebDav, +} + +#[derive(Clone)] +pub struct StoredObject { + pub key: String, + pub bytes: Vec, + pub content_type: Option, + pub content_disposition: Option, +} + +#[derive(Default)] +pub struct FakeStorage { + objects: Mutex>, +} + +#[async_trait] +impl ObjectStorage for FakeStorage { + async fn put_object( + &self, + key: &str, + bytes: Vec, + content_type: Option, + content_disposition: Option, + ) -> Result<()> { + let stored = StoredObject { + key: key.to_string(), + bytes, + content_type, + content_disposition, + }; + let mut guard = self.objects.lock().await; + guard.insert(stored.key.clone(), stored); + Ok(()) + } + + async fn presign_get_object( + &self, + key: &str, + expires_in: Duration, + _response_content_disposition: Option<&str>, + ) -> Result { + let guard = self.objects.lock().await; + ensure!(guard.contains_key(key), "object {key} missing"); + Ok(format!( + "https://fake-storage/{key}?expires_in={}", + expires_in.as_secs() + )) + } + + async fn get_object(&self, key: &str) -> Result> { + let guard = self.objects.lock().await; + guard + .get(key) + .map(|obj| obj.bytes.clone()) + .ok_or_else(|| anyhow!("object {key} missing")) + } + + async fn delete_object(&self, key: &str) -> Result<()> { + let mut guard = self.objects.lock().await; + guard.remove(key); + Ok(()) + } +} + +impl FakeStorage { + pub async fn get(&self, key: &str) -> Option { + let guard = self.objects.lock().await; + guard.get(key).cloned() + } + + pub async fn object_count(&self) -> usize { + let guard = self.objects.lock().await; + guard.len() + } +} + +pub struct TestApp { + pub state: AppState, + router: Router, + storage: Arc, +} + +impl TestApp { + pub async fn new() -> Result { + Self::with_config(|_| {}).await + } + + pub async fn with_config(configure: F) -> Result + where + F: FnOnce(&mut AppConfig), + { + let database_url = env::var("TEST_DATABASE_URL") + .context("TEST_DATABASE_URL must be set for integration tests")?; + + let mut config = AppConfig { + database_url: database_url.clone(), + database_max_pool_size: db::DEFAULT_MAX_POOL_SIZE, + server_host: "127.0.0.1".to_string(), + server_port: 0, + webdav_host: "127.0.0.1".to_string(), + webdav_port: 0, + jwt_secret: "test-secret".to_string(), + jwt_issuer: "test-issuer".to_string(), + jwt_audience: "test-audience".to_string(), + jwt_expiry_minutes: 60, + download_token_audience: "test-download".to_string(), + download_token_expiry_minutes: 60, + refresh_token_expiry_days: 30, + refresh_cookie_secure: false, + refresh_cookie_domain: None, + cors_allowed_origin: None, + proxy_downloads: false, + aws_endpoint_url: None, + aws_access_key_id: None, + aws_secret_access_key: None, + aws_region: "us-east-1".to_string(), + s3_bucket: "test-bucket".to_string(), + quickwit_endpoint: None, + quickwit_index: None, + worker_max_document_bytes: 200 * 1024 * 1024, + upload_body_limit_bytes: 128 * 1024 * 1024, + webauthn_rp_id: Some("localhost".to_string()), + webauthn_origin: Some("http://localhost".to_string()), + webauthn_rp_name: "Papercrate".to_string(), + }; + + configure(&mut config); + + let pool = db::init_pool_with_size(&config.database_url, config.database_max_pool_size)?; + prepare_database(&pool).await?; + + let storage = Arc::new(FakeStorage::default()); + let storage_for_state: Arc = storage.clone(); + let jwt = JwtService::from_config(&config)?; + let state = AppState::new(pool.clone(), config, storage_for_state, jwt); + let router = routes::create_router(state.clone()); + + let app = Self { + state, + router, + storage, + }; + + app.ensure_default_tenant().await?; + + Ok(app) + } + + pub async fn cleanup(&self) -> Result<()> { + let pool = self.state.pool.clone(); + let _ = tokio::task::spawn_blocking(move || -> Result<()> { + let mut conn = pool + .get() + .map_err(|err| anyhow!("failed to get cleanup connection: {err}"))?; + truncate_all(&mut conn)?; + Ok(()) + }) + .await + .context("cleanup task panicked")?; + + self.ensure_default_tenant().await?; + Ok(()) + } + + pub async fn tenant_id(&self) -> Result { + self.ensure_default_tenant().await + } + + pub fn storage(&self) -> Arc { + self.storage.clone() + } + + pub async fn storage_key_for(&self, key: &str) -> Result { + self.ensure_default_tenant().await?; + let tenant = self + .state + .tenants + .get_by_name(TEST_TENANT_NAME) + .map_err(|err| anyhow!("default tenant not found: {:?}", err))?; + let root = tenant + .storage_root + .clone() + .ok_or_else(|| anyhow!("default tenant missing storage root"))?; + Ok(format!("{}{}", root, key)) + } + + pub async fn insert_user(&self, username: &str, role: TestUserRole) -> Result { + let username = username.to_string(); + let tenant_id = self.ensure_default_tenant().await?; + let user_id = self + .with_conn(move |conn| { + let user = NewUser { + id: Uuid::new_v4(), + username, + }; + diesel::insert_into(crate::schema::users::table) + .values(&user) + .execute(conn) + .context("failed to insert user")?; + + let capabilities = match role { + TestUserRole::Owner => owner_capabilities(), + TestUserRole::Member => user_capabilities(), + TestUserRole::WebDav => webdav_capabilities(), + }; + + let capability_set = ensure_capability_set(conn, tenant_id, capabilities) + .map_err(|err| anyhow!("failed to ensure capability set: {:?}", err))?; + + let membership = NewUserMembership { + id: Uuid::new_v4(), + user_id: user.id, + tenant_id, + capability_set_id: Some(capability_set.id), + }; + + diesel::insert_into(crate::schema::user_memberships::table) + .values(&membership) + .execute(conn) + .context("failed to insert user membership")?; + Ok(user.id) + }) + .await?; + + Ok(user_id) + } + + pub async fn insert_passkey(&self, user_id: Uuid, nickname: Option<&str>) -> Result { + let passkey_id = Uuid::new_v4(); + let nickname = nickname.map(|value| value.to_string()); + self.with_conn(move |conn| { + let credential_id = passkey_id.as_bytes().to_vec(); + let public_key = passkey_id.as_bytes().iter().copied().collect::>(); + let passkey = NewUserPasskey { + id: passkey_id, + user_id, + credential_id, + public_key, + credential: json!({ "dummy": passkey_id.to_string() }), + sign_count: 0, + transports: vec![Some("usb".to_string())], + aaguid: None, + nickname, + }; + + diesel::insert_into(crate::schema::user_passkeys::table) + .values(&passkey) + .execute(conn) + .context("failed to insert passkey")?; + + Ok(passkey_id) + }) + .await + } + + async fn ensure_default_tenant(&self) -> Result { + let name_value = TEST_TENANT_NAME.to_string(); + let quickwit_enabled = self.state.config.quickwit_endpoint.is_some(); + let tenant_id = self + .with_conn(move |conn| { + use crate::schema::tenants::dsl as tenants_dsl; + + let existing = tenants_dsl::tenants + .filter(tenants_dsl::name.eq(&name_value)) + .first::(conn) + .optional() + .context("failed to load default tenant")?; + + let tenant_id = if let Some(current) = existing { + let desired_root = current + .storage_root + .clone() + .filter(|root| root.ends_with('/')) + .unwrap_or_else(|| format!("test-tenants/{}/", current.id)); + + if current.storage_root.as_deref() != Some(desired_root.as_str()) { + diesel::update(tenants_dsl::tenants.filter(tenants_dsl::id.eq(current.id))) + .set(tenants_dsl::storage_root.eq(Some(desired_root))) + .execute(conn) + .context("failed to update default tenant storage root")?; + } + + current.id + } else { + let new_id = Uuid::new_v4(); + let root = format!("test-tenants/{}/", new_id); + let quickwit_value = if quickwit_enabled { + Some(format!("documents-{}", new_id)) + } else { + None + }; + + diesel::insert_into(tenants_dsl::tenants) + .values(( + tenants_dsl::id.eq(new_id), + tenants_dsl::name.eq(&name_value), + tenants_dsl::storage_root.eq(Some(root)), + tenants_dsl::quickwit_index.eq(quickwit_value), + tenants_dsl::status.eq(TenantStatus::Active), + )) + .execute(conn) + .context("failed to insert default tenant")?; + + new_id + }; + + Ok(tenant_id) + }) + .await?; + + let mut conn = self + .state + .db_for_tenant(tenant_id) + .map_err(|err| anyhow!("failed to scope tenant connection: {err:?}"))?; + + ensure_capability_set(&mut conn, tenant_id, owner_capabilities()) + .map_err(|err| anyhow!("ensure owner capability set: {err:?}"))?; + ensure_capability_set(&mut conn, tenant_id, user_capabilities()) + .map_err(|err| anyhow!("ensure user capability set: {err:?}"))?; + ensure_capability_set(&mut conn, tenant_id, readonly_capabilities()) + .map_err(|err| anyhow!("ensure readonly capability set: {err:?}"))?; + ensure_capability_set(&mut conn, tenant_id, webdav_capabilities()) + .map_err(|err| anyhow!("ensure webdav capability set: {err:?}"))?; + + Ok(tenant_id) + } + + pub async fn login_token(&self, username: &str, _password: &str) -> Result { + let (access_token, _, _) = self.create_session(username).await?; + Ok(access_token) + } + + pub async fn create_session(&self, username: &str) -> Result<(String, String, Uuid)> { + let username = username.to_string(); + let state = self.state.clone(); + self.with_conn(move |conn| { + use crate::schema::capability_sets::dsl as capability_sets_dsl; + use crate::schema::tenants::dsl as tenants_dsl; + use crate::schema::user_memberships::dsl as memberships_dsl; + use crate::schema::users::dsl as users_dsl; + + let user: User = users_dsl::users + .filter(users_dsl::username.eq(&username)) + .first(conn)?; + + let membership: UserMembership = memberships_dsl::user_memberships + .filter(memberships_dsl::user_id.eq(user.id)) + .first(conn)?; + + let tenant: Tenant = tenants_dsl::tenants + .find(membership.tenant_id) + .first(conn)?; + + let capability_set_id = membership + .capability_set_id + .ok_or_else(|| anyhow!("membership missing capability set"))?; + + let cap_version = capability_sets_dsl::capability_sets + .find(capability_set_id) + .select(capability_sets_dsl::cap_version) + .first::(conn)?; + + let now = Utc::now(); + let session_id = Uuid::new_v4(); + let access_token = state + .jwt + .generate_token(AccessTokenContext { + user_id: user.id, + tenant_id: tenant.id, + username: user.username.clone(), + principal_kind: PrincipalKind::UserSession, + principal_id: session_id, + capability_set_id, + cap_version, + }) + .map_err(|err| anyhow!(err))?; + + let session_value = generate_session_token(); + let session_hash = hash_session_token(&session_value); + let refresh_expires_at = + now + ChronoDuration::days(state.config.refresh_token_expiry_days); + + let new_session = NewUserSession { + id: session_id, + user_id: user.id, + token_hash: session_hash, + issued_at: now.naive_utc(), + expires_at: refresh_expires_at.naive_utc(), + tenant_id: tenant.id, + }; + + diesel::insert_into(session_dsl::user_sessions) + .values(&new_session) + .execute(conn)?; + + let cookie = format!("refresh_token={session_value}"); + Ok((access_token, cookie, tenant.id)) + }) + .await + } + + pub async fn clear_jobs(&self) -> Result<()> { + self.with_conn(|conn| { + use crate::schema::jobs::dsl::jobs as jobs_table; + diesel::delete(jobs_table) + .execute(conn) + .context("failed to clear jobs")?; + Ok(()) + }) + .await + } + + pub async fn jobs_by_type(&self, ty: &str) -> Result> { + let ty = ty.to_string(); + self.with_conn(move |conn| { + use crate::schema::jobs::dsl::{job_type as job_type_col, jobs as jobs_table}; + let rows = jobs_table + .filter(job_type_col.eq(&ty)) + .load::(conn) + .context("failed to load jobs")?; + Ok(rows) + }) + .await + } + + pub async fn post_json( + &self, + path: &str, + payload: &T, + token: Option<&str>, + ) -> Result> { + self.post_json_with_cookie(path, payload, token, None).await + } + + pub async fn post_json_with_cookie( + &self, + path: &str, + payload: &T, + token: Option<&str>, + cookie: Option<&str>, + ) -> Result> { + let body = serde_json::to_vec(payload)?; + let mut builder = Request::builder() + .method(Method::POST) + .uri(path) + .header("content-type", "application/json"); + if let Some(token) = token { + builder = builder.header("authorization", format!("Bearer {token}")); + } + if let Some(cookie) = cookie { + builder = builder.header(header::COOKIE, cookie); + } + let request = builder.body(Body::from(body))?; + Ok(self + .router + .clone() + .oneshot(request) + .await + .expect("infallible response")) + } + + pub async fn patch_json( + &self, + path: &str, + payload: &T, + token: Option<&str>, + ) -> Result> { + let body = serde_json::to_vec(payload)?; + let mut builder = Request::builder() + .method(Method::PATCH) + .uri(path) + .header("content-type", "application/json"); + if let Some(token) = token { + builder = builder.header("authorization", format!("Bearer {token}")); + } + let request = builder.body(Body::from(body))?; + Ok(self + .router + .clone() + .oneshot(request) + .await + .expect("infallible response")) + } + + pub async fn get(&self, path: &str, token: Option<&str>) -> Result> { + let mut builder = Request::builder().method(Method::GET).uri(path); + if let Some(token) = token { + builder = builder.header("authorization", format!("Bearer {token}")); + } + let request = builder.body(Body::empty())?; + Ok(self + .router + .clone() + .oneshot(request) + .await + .expect("infallible response")) + } + + pub async fn delete(&self, path: &str, token: Option<&str>) -> Result> { + let builder = Request::builder().method(Method::DELETE).uri(path); + let builder = if let Some(token) = token { + builder.header("authorization", format!("Bearer {token}")) + } else { + builder + }; + let request = builder.body(Body::empty())?; + Ok(self + .router + .clone() + .oneshot(request) + .await + .expect("infallible response")) + } + + pub async fn upload_document( + &self, + path: &str, + filename: &str, + content_type: &str, + data: &[u8], + folder_id: Option, + token: &str, + ) -> Result> { + let extras = UploadExtras::empty(); + self.upload_document_with_extras( + path, + filename, + content_type, + data, + folder_id, + extras, + token, + ) + .await + } + + pub async fn upload_document_with_options( + &self, + path: &str, + filename: &str, + content_type: &str, + data: &[u8], + folder_id: Option, + title: Option<&str>, + metadata_json: Option<&str>, + token: &str, + ) -> Result> { + let extras = UploadExtras { + title, + metadata_json, + tag_ids_json: None, + correspondents_json: None, + issued_at: None, + skip_existing: None, + }; + self.upload_document_with_extras( + path, + filename, + content_type, + data, + folder_id, + extras, + token, + ) + .await + } + + pub async fn upload_document_with_extras( + &self, + path: &str, + filename: &str, + content_type: &str, + data: &[u8], + folder_id: Option, + extras: UploadExtras<'_>, + token: &str, + ) -> Result> { + let boundary = format!("boundary-{}", Uuid::new_v4()); + let mut body = Vec::new(); + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend( + format!( + "Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n", + filename + ) + .as_bytes(), + ); + body.extend(format!("Content-Type: {}\r\n\r\n", content_type).as_bytes()); + body.extend(data); + body.extend(b"\r\n"); + + if let Some(folder) = folder_id { + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend(b"Content-Disposition: form-data; name=\"folder_id\"\r\n\r\n"); + body.extend(folder.to_string().as_bytes()); + body.extend(b"\r\n"); + } + + if let Some(title_value) = extras.title { + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend(b"Content-Disposition: form-data; name=\"title\"\r\n\r\n"); + body.extend(title_value.as_bytes()); + body.extend(b"\r\n"); + } + + if let Some(metadata_value) = extras.metadata_json { + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend(b"Content-Disposition: form-data; name=\"metadata\"\r\n\r\n"); + body.extend(metadata_value.as_bytes()); + body.extend(b"\r\n"); + } + + if let Some(tag_ids_value) = extras.tag_ids_json { + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend(b"Content-Disposition: form-data; name=\"tag_ids\"\r\n\r\n"); + body.extend(tag_ids_value.as_bytes()); + body.extend(b"\r\n"); + } + + if let Some(correspondents_value) = extras.correspondents_json { + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend(b"Content-Disposition: form-data; name=\"correspondents\"\r\n\r\n"); + body.extend(correspondents_value.as_bytes()); + body.extend(b"\r\n"); + } + + if let Some(issued_at_value) = extras.issued_at { + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend(b"Content-Disposition: form-data; name=\"issued_at\"\r\n\r\n"); + body.extend(issued_at_value.as_bytes()); + body.extend(b"\r\n"); + } + + if let Some(skip_flag) = extras.skip_existing { + body.extend(format!("--{boundary}\r\n").as_bytes()); + body.extend(b"Content-Disposition: form-data; name=\"skip_existing\"\r\n\r\n"); + body.extend(if skip_flag { + b"true".as_ref() + } else { + b"false".as_ref() + }); + body.extend(b"\r\n"); + } + + body.extend(format!("--{boundary}--\r\n").as_bytes()); + + let builder = Request::builder() + .method(Method::POST) + .uri(path) + .header( + "content-type", + format!("multipart/form-data; boundary={boundary}"), + ) + .header("authorization", format!("Bearer {token}")); + + let request = builder.body(Body::from(body))?; + Ok(self + .router + .clone() + .oneshot(request) + .await + .expect("infallible response")) + } + + pub async fn with_conn(&self, f: F) -> Result + where + F: FnOnce(&mut PgConnection) -> Result + Send + 'static, + T: Send + 'static, + { + let pool = self.state.pool.clone(); + tokio::task::spawn_blocking(move || { + let mut conn = pool + .get() + .map_err(|err| anyhow!("failed to get database connection: {err}"))?; + f(&mut conn) + }) + .await + .context("connection task panicked")? + } +} + +pub struct UploadExtras<'a> { + pub title: Option<&'a str>, + pub metadata_json: Option<&'a str>, + pub tag_ids_json: Option<&'a str>, + pub correspondents_json: Option<&'a str>, + pub issued_at: Option<&'a str>, + pub skip_existing: Option, +} + +impl<'a> UploadExtras<'a> { + pub fn empty() -> Self { + Self { + title: None, + metadata_json: None, + tag_ids_json: None, + correspondents_json: None, + issued_at: None, + skip_existing: None, + } + } +} + +pub async fn acquire_db_lock() -> tokio::sync::MutexGuard<'static, ()> { + DB_LOCK.lock().await +} + +pub async fn body_to_vec(body: Body) -> Result> { + let collected = body + .collect() + .await + .map_err(|err| anyhow!("failed to read response body: {err}"))?; + Ok(collected.to_bytes().to_vec()) +} + +#[cfg(test)] +mod helper_tests { + use super::*; + + #[tokio::test] + async fn create_session_and_login_token_provide_access() -> Result<()> { + let _lock = acquire_db_lock().await; + let app = TestApp::new().await?; + + let username = "helper-login"; + let password = "irrelevant"; + app.insert_user(username, TestUserRole::Owner).await?; + + let (access, refresh, refresh_id) = app.create_session(username).await?; + assert!(!access.is_empty(), "access token should not be empty"); + assert!(!refresh.is_empty(), "refresh token should not be empty"); + assert_ne!( + refresh_id, + Uuid::nil(), + "refresh token id should be assigned" + ); + + let bearer = app.login_token(username, password).await?; + assert!(!bearer.is_empty(), "login_token must yield bearer"); + + app.cleanup().await?; + Ok(()) + } + + #[tokio::test] + async fn insert_passkey_and_upload_with_options_succeeds() -> Result<()> { + let _lock = acquire_db_lock().await; + let app = TestApp::new().await?; + let username = "helper-passkey"; + let password = "unused"; + let user_id = app.insert_user(username, TestUserRole::Owner).await?; + + let passkey_id = app.insert_passkey(user_id, Some("Laptop")).await?; + assert_ne!(passkey_id, Uuid::nil()); + + let bearer = app.login_token(username, password).await?; + let response = app + .upload_document_with_options( + "/api/documents", + "helper.txt", + "text/plain", + b"helper-content", + None, + Some("Helper Note"), + Some("{\"category\":\"note\"}"), + &bearer, + ) + .await?; + assert!(response.status().is_success()); + + app.cleanup().await?; + Ok(()) + } +} + +async fn prepare_database(pool: &PgPool) -> Result<()> { + let pool = pool.clone(); + tokio::task::spawn_blocking(move || -> Result<()> { + let mut conn = pool + .get() + .map_err(|err| anyhow!("failed to acquire connection: {err}"))?; + conn.batch_execute(RESET_DATABASE_SQL) + .map_err(|err| anyhow!("failed to reset schema: {err}"))?; + conn.batch_execute("DROP TABLE IF EXISTS __diesel_schema_migrations;") + .map_err(|err| anyhow!("failed to drop diesel schema table: {err}"))?; + conn.run_pending_migrations(MIGRATIONS) + .map_err(|err| anyhow!("failed to run migrations: {err}"))?; + truncate_all(&mut conn)?; + Ok(()) + }) + .await + .context("migration task panicked")? +} + +fn truncate_all(conn: &mut PgConnection) -> Result<()> { + conn.batch_execute( + "TRUNCATE TABLE \ + tenant.document_asset_objects, \ + tenant.document_assets, \ + tenant.document_correspondents, \ + tenant.correspondents, \ + tenant.document_tags, \ + tenant.document_versions, \ + tenant.documents, \ + tenant.folders, \ + shared.jobs, \ + tenant.user_sessions, \ + tenant.tags, \ + tenant.api_tokens, \ + shared.webauthn_challenges, \ + shared.user_passkeys, \ + tenant.user_memberships, \ + shared.users, \ + shared.magic_tokens, \ + shared.tenants \ + RESTART IDENTITY CASCADE;", + ) + .context("failed to truncate tables")?; + Ok(()) +} + +fn generate_session_token() -> String { + let mut bytes = [0u8; 32]; + OsRng + .try_fill_bytes(&mut bytes) + .expect("failed to read random bytes"); + hex::encode(bytes) +} + +fn hash_session_token(value: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(value.as_bytes()); + hex::encode(hasher.finalize()) +} diff --git a/backend/tests/api_tokens_flow.rs b/backend/tests/api_tokens_flow.rs index 43d10df..032f23d 100644 --- a/backend/tests/api_tokens_flow.rs +++ b/backend/tests/api_tokens_flow.rs @@ -1,15 +1,16 @@ -mod common; - -use anyhow::Result; +use anyhow::{Context, Result}; use axum::body::Body; use axum::http::{header, Method, Request, StatusCode}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use diesel::prelude::*; -use papercrate::models::ApiToken; +use diesel::OptionalExtension; +use papercrate::auth::capability_sets; +use papercrate::models::{ApiCapability, ApiToken}; use papercrate::routes::webdav; use papercrate::schema::api_tokens; +use papercrate::schema::capability_sets::dsl as capability_sets_dsl; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use serde::Deserialize; use serde_json::json; use tower::ServiceExt; @@ -59,12 +60,6 @@ struct TenantView { name: String, } -#[derive(Debug, Deserialize)] -struct CapabilitySetSummary { - id: Uuid, - slug: String, -} - #[tokio::test] async fn api_token_crud_flow() -> Result<()> { let _guard = acquire_db_lock().await; @@ -356,46 +351,42 @@ async fn ensure_capability_set_slug( slug: &str, capabilities: &[&str], ) -> Result { - if let Some(existing) = find_capability_set_slug(app, access_token, slug).await? { - return Ok(existing); - } + let claims = app + .state + .jwt + .verify_token(access_token) + .context("failed to decode access token claims")?; + let tenant_id = claims.tenant_id; + let slug = slug.to_string(); + let desired_capabilities = capabilities + .iter() + .map(|value| { + value + .parse::() + .map_err(|err| anyhow::anyhow!("invalid capability '{value}': {err}")) + }) + .collect::>>()?; - let response = app - .post_json( - "/api/capability-sets", - &json!({ - "slug": slug, - "capabilities": capabilities, - }), - Some(access_token), - ) - .await?; - assert_eq!(response.status(), StatusCode::CREATED); - let body = body_to_vec(response.into_body()).await?; - let summary: CapabilitySetSummary = serde_json::from_slice(&body)?; - Ok(summary.id) -} + let caps_for_insert = desired_capabilities.clone(); + app.with_conn(move |conn| { + if let Some(existing) = capability_sets_dsl::capability_sets + .filter(capability_sets_dsl::tenant_id.eq(tenant_id)) + .filter(capability_sets_dsl::slug.eq(&slug)) + .select(capability_sets_dsl::id) + .first::(conn) + .optional()? + { + return Ok(existing); + } -async fn find_capability_set_slug( - app: &TestApp, - access_token: &str, - slug: &str, -) -> Result> { - let sets = list_capability_sets(app, access_token).await?; - Ok(sets - .into_iter() - .find(|set| set.slug == slug) - .map(|set| set.id)) -} - -async fn list_capability_sets( - app: &TestApp, - access_token: &str, -) -> Result> { - let response = app.get("/api/capability-sets", Some(access_token)).await?; - assert_eq!(response.status(), StatusCode::OK); - let body = body_to_vec(response.into_body()).await?; - Ok(serde_json::from_slice(&body)?) + let created = + capability_sets::create_capability_set(conn, tenant_id, &slug, caps_for_insert) + .map_err(|err| { + anyhow::anyhow!("failed to create capability set '{slug}': {err:?}") + })?; + Ok(created.id) + }) + .await } async fn exchange_token(app: &TestApp, api_token: &str) -> Result { let response = app diff --git a/backend/tests/auth_flow.rs b/backend/tests/auth_flow.rs index 1a03ae2..4c9f395 100644 --- a/backend/tests/auth_flow.rs +++ b/backend/tests/auth_flow.rs @@ -1,9 +1,6 @@ -mod common; - use anyhow::{anyhow, Context, Result}; use axum::http::{header::SET_COOKIE, StatusCode}; use chrono::{Duration as ChronoDuration, Utc}; -use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use diesel::prelude::*; use papercrate::auth::capability_sets::{ensure_capability_set, owner_capabilities}; use papercrate::auth::jwt::{AccessTokenContext, PrincipalKind}; @@ -14,8 +11,8 @@ use papercrate::auth::passkeys::{ use papercrate::models::{NewUserMembership, NewUserSession, TenantStatus, UserPasskey}; use papercrate::openapi::schemas::PasskeySummary; use papercrate::schema::{capability_sets, tenants, user_memberships, user_sessions, users}; -use rand::rngs::OsRng; -use rand::RngCore; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; +use rand::{rngs::OsRng, TryRngCore}; use serde::Deserialize; use serde_json::json; use sha2::{Digest, Sha256}; @@ -55,14 +52,6 @@ struct SignupStartResponse { challenge: RegistrationChallengeResponse, } -#[derive(Deserialize)] -struct TenantSelectionResponse { - #[serde(rename = "access_token")] - _access_token: String, - #[serde(rename = "tenants")] - _tenants: Vec, -} - #[derive(Deserialize)] struct TenantListResponse { tenants: Vec, @@ -226,8 +215,9 @@ async fn passkey_login_start_requires_passkey() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; - let password = "secret"; - app.insert_user("passkey-login", TestUserRole::Owner).await?; + let _password = "secret"; + app.insert_user("passkey-login", TestUserRole::Owner) + .await?; let payload = PasskeyLoginStartPayload { username: "passkey-login".to_string(), @@ -288,7 +278,9 @@ async fn list_passkeys_returns_entries() -> Result<()> { let app = TestApp::new().await?; let password = "secret"; - let user_id = app.insert_user("passkey-owner", TestUserRole::Owner).await?; + let user_id = app + .insert_user("passkey-owner", TestUserRole::Owner) + .await?; app.insert_passkey(user_id, Some("Laptop")).await?; let (session, _) = login_with_session(&app, "passkey-owner", password).await?; @@ -314,7 +306,9 @@ async fn delete_passkey_soft_revokes() -> Result<()> { let app = TestApp::new().await?; let password = "secret"; - let user_id = app.insert_user("passkey-delete", TestUserRole::Owner).await?; + let user_id = app + .insert_user("passkey-delete", TestUserRole::Owner) + .await?; let passkey_id = app.insert_passkey(user_id, Some("Phone")).await?; app.insert_passkey(user_id, Some("Backup")).await?; let (session, _) = login_with_session(&app, "passkey-delete", password).await?; @@ -349,7 +343,9 @@ async fn delete_passkey_prevents_last() -> Result<()> { let app = TestApp::new().await?; let password = "secret"; - let user_id = app.insert_user("passkey-guard", TestUserRole::Owner).await?; + let user_id = app + .insert_user("passkey-guard", TestUserRole::Owner) + .await?; let first_id = app.insert_passkey(user_id, Some("Key A")).await?; let last_id = app.insert_passkey(user_id, Some("Key B")).await?; let (session, _) = login_with_session(&app, "passkey-guard", password).await?; @@ -408,7 +404,7 @@ async fn login_rejects_invalid_password() -> Result<()> { let _lock = acquire_db_lock().await; let app = TestApp::new().await?; - let password = "valid"; + let _password = "valid"; app.insert_user("robin", TestUserRole::Owner).await?; let payload = json!({ "username": "robin", "password": "wrong" }); @@ -671,7 +667,9 @@ fn extract_refresh_cookie(headers: &axum::http::HeaderMap) -> Result { fn generate_session_token() -> String { let mut bytes = [0u8; 32]; - OsRng.fill_bytes(&mut bytes); + OsRng + .try_fill_bytes(&mut bytes) + .expect("failed to read random bytes"); hex::encode(bytes) } diff --git a/backend/tests/capability_guards_flow.rs b/backend/tests/capability_guards_flow.rs index 91f280c..8072b1a 100644 --- a/backend/tests/capability_guards_flow.rs +++ b/backend/tests/capability_guards_flow.rs @@ -1,10 +1,8 @@ -mod common; - use anyhow::{anyhow, Result}; use axum::http::StatusCode; -use common::{TestApp, TestUserRole}; use diesel::prelude::*; use papercrate::models::ApiCapability; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use serde_json::json; async fn set_user_capabilities( @@ -38,7 +36,7 @@ async fn set_user_capabilities( #[tokio::test] async fn documents_routes_enforce_capabilities() -> Result<()> { - let _lock = common::acquire_db_lock().await; + let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "limited-docs"; @@ -61,12 +59,12 @@ async fn documents_routes_enforce_capabilities() -> Result<()> { ) .await?; assert_eq!(upload.status(), StatusCode::FORBIDDEN); - let upload_body = common::body_to_vec(upload.into_body()).await?; + let upload_body = body_to_vec(upload.into_body()).await?; assert!(String::from_utf8_lossy(&upload_body).contains("missing")); let capability_sets = app.get("/api/capability-sets", Some(&token)).await?; assert_eq!(capability_sets.status(), StatusCode::FORBIDDEN); - let caps_body = common::body_to_vec(capability_sets.into_body()).await?; + let caps_body = body_to_vec(capability_sets.into_body()).await?; assert!(String::from_utf8_lossy(&caps_body).contains("missing")); app.cleanup().await?; @@ -75,11 +73,11 @@ async fn documents_routes_enforce_capabilities() -> Result<()> { #[tokio::test] async fn capability_set_routes_require_write_privilege() -> Result<()> { - let _lock = common::acquire_db_lock().await; + let _lock = acquire_db_lock().await; let app = TestApp::new().await?; let password = "caps-reader"; - let user_id = app.insert_user("caps-reader", TestUserRole::Owner).await?; + let user_id = app.insert_user("caps-reader", TestUserRole::Member).await?; set_user_capabilities(&app, user_id, &[ApiCapability::CapabilitySetsRead]).await?; let token = app.login_token("caps-reader", password).await?; @@ -98,7 +96,7 @@ async fn capability_set_routes_require_write_privilege() -> Result<()> { ) .await?; assert_eq!(create.status(), StatusCode::FORBIDDEN); - let create_body = common::body_to_vec(create.into_body()).await?; + let create_body = body_to_vec(create.into_body()).await?; assert!(String::from_utf8_lossy(&create_body).contains("missing")); app.cleanup().await?; diff --git a/backend/tests/capability_sets_flow.rs b/backend/tests/capability_sets_flow.rs index 903922b..a4ebadd 100644 --- a/backend/tests/capability_sets_flow.rs +++ b/backend/tests/capability_sets_flow.rs @@ -1,8 +1,6 @@ -mod common; - use anyhow::Result; use axum::http::StatusCode; -use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use serde::Deserialize; use serde_json::json; use uuid::Uuid; diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 473eca4..690c28d 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -1,902 +1 @@ -use std::collections::HashMap; -use std::env; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::{anyhow, ensure, Context, Result}; -use async_trait::async_trait; -use axum::body::Body; -use axum::http::{header, Method, Request}; -use axum::Router; -use chrono::{Duration as ChronoDuration, Utc}; -use diesel::connection::SimpleConnection; -use diesel::prelude::*; -use diesel::OptionalExtension; -use diesel::PgConnection; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use http_body_util::BodyExt; -use once_cell::sync::Lazy; -use papercrate::auth::capability_sets::{ - ensure_capability_set, owner_capabilities, readonly_capabilities, user_capabilities, - webdav_capabilities, -}; -use papercrate::auth::jwt::{AccessTokenContext, JwtService, PrincipalKind}; -use papercrate::config::AppConfig; -use papercrate::db::{self, PgPool}; -use papercrate::models::{ - Job, NewUser, NewUserMembership, NewUserPasskey, NewUserSession, Tenant, TenantStatus, User, - UserMembership, -}; -use papercrate::routes; -use papercrate::schema::user_sessions::dsl as session_dsl; -use papercrate::state::AppState; -use papercrate::storage::ObjectStorage; -use rand::rngs::OsRng; -use rand::RngCore; -use serde::Serialize; -use serde_json::{self, json}; -use sha2::{Digest, Sha256}; -use tokio::sync::Mutex; -use tower::util::ServiceExt; -use uuid::Uuid; - -const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); -const RESET_DATABASE_SQL: &str = "DROP SCHEMA IF EXISTS tenant CASCADE;\n\ - DROP SCHEMA IF EXISTS shared CASCADE;\n\ - DROP SCHEMA IF EXISTS public CASCADE;\n\ - CREATE SCHEMA public;\n\ - GRANT ALL ON SCHEMA public TO public;"; - -static DB_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); - -const TEST_TENANT_NAME: &str = "test_tenant"; - -#[derive(Clone, Copy, Debug)] -pub enum TestUserRole { - Owner, - Member, - WebDav, -} - -#[allow(dead_code)] -#[derive(Clone)] -pub struct StoredObject { - pub key: String, - pub bytes: Vec, - pub content_type: Option, - pub content_disposition: Option, -} - -#[derive(Default)] -pub struct FakeStorage { - objects: Mutex>, -} - -#[async_trait] -impl ObjectStorage for FakeStorage { - async fn put_object( - &self, - key: &str, - bytes: Vec, - content_type: Option, - content_disposition: Option, - ) -> Result<()> { - let stored = StoredObject { - key: key.to_string(), - bytes, - content_type, - content_disposition, - }; - let mut guard = self.objects.lock().await; - guard.insert(stored.key.clone(), stored); - Ok(()) - } - - async fn presign_get_object( - &self, - key: &str, - expires_in: Duration, - _response_content_disposition: Option<&str>, - ) -> Result { - let guard = self.objects.lock().await; - ensure!(guard.contains_key(key), "object {key} missing"); - Ok(format!( - "https://fake-storage/{key}?expires_in={}", - expires_in.as_secs() - )) - } - - async fn get_object(&self, key: &str) -> Result> { - let guard = self.objects.lock().await; - guard - .get(key) - .map(|obj| obj.bytes.clone()) - .ok_or_else(|| anyhow!("object {key} missing")) - } - - async fn delete_object(&self, key: &str) -> Result<()> { - let mut guard = self.objects.lock().await; - guard.remove(key); - Ok(()) - } -} - -impl FakeStorage { - #[allow(dead_code)] - pub async fn get(&self, key: &str) -> Option { - let guard = self.objects.lock().await; - guard.get(key).cloned() - } - - #[allow(dead_code)] - pub async fn object_count(&self) -> usize { - let guard = self.objects.lock().await; - guard.len() - } -} - -pub struct TestApp { - pub state: AppState, - router: Router, - storage: Arc, -} - -impl TestApp { - pub async fn new() -> Result { - Self::with_config(|_| {}).await - } - - pub async fn with_config(configure: F) -> Result - where - F: FnOnce(&mut AppConfig), - { - let database_url = env::var("TEST_DATABASE_URL") - .context("TEST_DATABASE_URL must be set for integration tests")?; - - let mut config = AppConfig { - database_url: database_url.clone(), - database_max_pool_size: db::DEFAULT_MAX_POOL_SIZE, - server_host: "127.0.0.1".to_string(), - server_port: 0, - webdav_host: "127.0.0.1".to_string(), - webdav_port: 0, - jwt_secret: "test-secret".to_string(), - jwt_issuer: "test-issuer".to_string(), - jwt_audience: "test-audience".to_string(), - jwt_expiry_minutes: 60, - download_token_audience: "test-download".to_string(), - download_token_expiry_minutes: 60, - refresh_token_expiry_days: 30, - refresh_cookie_secure: false, - refresh_cookie_domain: None, - cors_allowed_origin: None, - proxy_downloads: false, - aws_endpoint_url: None, - aws_access_key_id: None, - aws_secret_access_key: None, - aws_region: "us-east-1".to_string(), - s3_bucket: "test-bucket".to_string(), - quickwit_endpoint: None, - quickwit_index: None, - worker_max_document_bytes: 200 * 1024 * 1024, - upload_body_limit_bytes: 128 * 1024 * 1024, - webauthn_rp_id: Some("localhost".to_string()), - webauthn_origin: Some("http://localhost".to_string()), - webauthn_rp_name: "Papercrate".to_string(), - }; - - configure(&mut config); - - let pool = db::init_pool_with_size(&config.database_url, config.database_max_pool_size)?; - prepare_database(&pool).await?; - - let storage = Arc::new(FakeStorage::default()); - let storage_for_state: Arc = storage.clone(); - let jwt = JwtService::from_config(&config)?; - let state = AppState::new(pool.clone(), config, storage_for_state, jwt); - let router = routes::create_router(state.clone()); - - let app = Self { - state, - router, - storage, - }; - - app.ensure_default_tenant().await?; - - Ok(app) - } - - pub async fn cleanup(&self) -> Result<()> { - let pool = self.state.pool.clone(); - let _ = tokio::task::spawn_blocking(move || -> Result<()> { - let mut conn = pool - .get() - .map_err(|err| anyhow!("failed to get cleanup connection: {err}"))?; - truncate_all(&mut conn)?; - Ok(()) - }) - .await - .context("cleanup task panicked")?; - - self.ensure_default_tenant().await?; - Ok(()) - } - - #[allow(dead_code)] - pub async fn tenant_id(&self) -> Result { - self.ensure_default_tenant().await - } - - #[allow(dead_code)] - pub fn storage(&self) -> Arc { - self.storage.clone() - } - - #[allow(dead_code)] - pub async fn storage_key_for(&self, key: &str) -> Result { - self.ensure_default_tenant().await?; - let tenant = self - .state - .tenants - .get_by_name(TEST_TENANT_NAME) - .map_err(|err| anyhow!("default tenant not found: {:?}", err))?; - let root = tenant - .storage_root - .clone() - .ok_or_else(|| anyhow!("default tenant missing storage root"))?; - Ok(format!("{}{}", root, key)) - } - - pub async fn insert_user(&self, username: &str, role: TestUserRole) -> Result { - let username = username.to_string(); - let tenant_id = self.ensure_default_tenant().await?; - let user_id = self - .with_conn(move |conn| { - let user = NewUser { - id: Uuid::new_v4(), - username, - }; - diesel::insert_into(papercrate::schema::users::table) - .values(&user) - .execute(conn) - .context("failed to insert user")?; - - let capabilities = match role { - TestUserRole::Owner => owner_capabilities(), - TestUserRole::Member => user_capabilities(), - TestUserRole::WebDav => webdav_capabilities(), - }; - - let capability_set = ensure_capability_set(conn, tenant_id, capabilities) - .map_err(|err| anyhow!("failed to ensure capability set: {:?}", err))?; - - let membership = NewUserMembership { - id: Uuid::new_v4(), - user_id: user.id, - tenant_id, - capability_set_id: Some(capability_set.id), - }; - - diesel::insert_into(papercrate::schema::user_memberships::table) - .values(&membership) - .execute(conn) - .context("failed to insert user membership")?; - Ok(user.id) - }) - .await?; - - Ok(user_id) - } - - #[cfg(test)] - pub async fn insert_passkey(&self, user_id: Uuid, nickname: Option<&str>) -> Result { - let passkey_id = Uuid::new_v4(); - let nickname = nickname.map(|value| value.to_string()); - self.with_conn(move |conn| { - let credential_id = passkey_id.as_bytes().to_vec(); - let public_key = passkey_id.as_bytes().iter().copied().collect::>(); - let passkey = NewUserPasskey { - id: passkey_id, - user_id, - credential_id, - public_key, - credential: json!({ "dummy": passkey_id.to_string() }), - sign_count: 0, - transports: vec![Some("usb".to_string())], - aaguid: None, - nickname, - }; - - diesel::insert_into(papercrate::schema::user_passkeys::table) - .values(&passkey) - .execute(conn) - .context("failed to insert passkey")?; - - Ok(passkey_id) - }) - .await - } - - async fn ensure_default_tenant(&self) -> Result { - let name_value = TEST_TENANT_NAME.to_string(); - let quickwit_enabled = self.state.config.quickwit_endpoint.is_some(); - let tenant_id = self - .with_conn(move |conn| { - use papercrate::schema::tenants::dsl as tenants_dsl; - - let existing = tenants_dsl::tenants - .filter(tenants_dsl::name.eq(&name_value)) - .first::(conn) - .optional() - .context("failed to load default tenant")?; - - let tenant_id = if let Some(current) = existing { - let desired_root = current - .storage_root - .clone() - .filter(|root| root.ends_with('/')) - .unwrap_or_else(|| format!("test-tenants/{}/", current.id)); - - if current.storage_root.as_deref() != Some(desired_root.as_str()) { - diesel::update(tenants_dsl::tenants.filter(tenants_dsl::id.eq(current.id))) - .set(tenants_dsl::storage_root.eq(Some(desired_root))) - .execute(conn) - .context("failed to update default tenant storage root")?; - } - - current.id - } else { - let new_id = Uuid::new_v4(); - let root = format!("test-tenants/{}/", new_id); - let quickwit_value = if quickwit_enabled { - Some(format!("documents-{}", new_id)) - } else { - None - }; - - diesel::insert_into(tenants_dsl::tenants) - .values(( - tenants_dsl::id.eq(new_id), - tenants_dsl::name.eq(&name_value), - tenants_dsl::storage_root.eq(Some(root)), - tenants_dsl::quickwit_index.eq(quickwit_value), - tenants_dsl::status.eq(TenantStatus::Active), - )) - .execute(conn) - .context("failed to insert default tenant")?; - - new_id - }; - - Ok(tenant_id) - }) - .await?; - - let mut conn = self - .state - .db_for_tenant(tenant_id) - .map_err(|err| anyhow!("failed to scope tenant connection: {err:?}"))?; - - ensure_capability_set(&mut conn, tenant_id, owner_capabilities()) - .map_err(|err| anyhow!("ensure owner capability set: {err:?}"))?; - ensure_capability_set(&mut conn, tenant_id, user_capabilities()) - .map_err(|err| anyhow!("ensure user capability set: {err:?}"))?; - ensure_capability_set(&mut conn, tenant_id, readonly_capabilities()) - .map_err(|err| anyhow!("ensure readonly capability set: {err:?}"))?; - ensure_capability_set(&mut conn, tenant_id, webdav_capabilities()) - .map_err(|err| anyhow!("ensure webdav capability set: {err:?}"))?; - - Ok(tenant_id) - } - - pub async fn login_token(&self, username: &str, _password: &str) -> Result { - let (access_token, _, _) = self.create_session(username).await?; - Ok(access_token) - } - - pub async fn create_session(&self, username: &str) -> Result<(String, String, Uuid)> { - let username = username.to_string(); - let state = self.state.clone(); - self.with_conn(move |conn| { - use papercrate::schema::capability_sets::dsl as capability_sets_dsl; - use papercrate::schema::tenants::dsl as tenants_dsl; - use papercrate::schema::user_memberships::dsl as memberships_dsl; - use papercrate::schema::users::dsl as users_dsl; - - let user: User = users_dsl::users - .filter(users_dsl::username.eq(&username)) - .first(conn)?; - - let membership: UserMembership = memberships_dsl::user_memberships - .filter(memberships_dsl::user_id.eq(user.id)) - .first(conn)?; - - let tenant: Tenant = tenants_dsl::tenants - .find(membership.tenant_id) - .first(conn)?; - - let capability_set_id = membership - .capability_set_id - .ok_or_else(|| anyhow!("membership missing capability set"))?; - - let cap_version = capability_sets_dsl::capability_sets - .find(capability_set_id) - .select(capability_sets_dsl::cap_version) - .first::(conn)?; - - let now = Utc::now(); - let session_id = Uuid::new_v4(); - let access_token = state - .jwt - .generate_token(AccessTokenContext { - user_id: user.id, - tenant_id: tenant.id, - username: user.username.clone(), - principal_kind: PrincipalKind::UserSession, - principal_id: session_id, - capability_set_id, - cap_version, - }) - .map_err(|err| anyhow!(err))?; - - let session_value = generate_session_token(); - let session_hash = hash_session_token(&session_value); - let refresh_expires_at = - now + ChronoDuration::days(state.config.refresh_token_expiry_days); - - let new_session = NewUserSession { - id: session_id, - user_id: user.id, - token_hash: session_hash, - issued_at: now.naive_utc(), - expires_at: refresh_expires_at.naive_utc(), - tenant_id: tenant.id, - }; - - diesel::insert_into(session_dsl::user_sessions) - .values(&new_session) - .execute(conn)?; - - let cookie = format!("refresh_token={session_value}"); - Ok((access_token, cookie, tenant.id)) - }) - .await - } - - #[allow(dead_code)] - pub async fn clear_jobs(&self) -> Result<()> { - self.with_conn(|conn| { - use papercrate::schema::jobs::dsl::jobs as jobs_table; - diesel::delete(jobs_table) - .execute(conn) - .context("failed to clear jobs")?; - Ok(()) - }) - .await - } - - #[allow(dead_code)] - pub async fn jobs_by_type(&self, ty: &str) -> Result> { - let ty = ty.to_string(); - self.with_conn(move |conn| { - use papercrate::schema::jobs::dsl::{job_type as job_type_col, jobs as jobs_table}; - let rows = jobs_table - .filter(job_type_col.eq(&ty)) - .load::(conn) - .context("failed to load jobs")?; - Ok(rows) - }) - .await - } - - pub async fn post_json( - &self, - path: &str, - payload: &T, - token: Option<&str>, - ) -> Result> { - self.post_json_with_cookie(path, payload, token, None).await - } - - pub async fn post_json_with_cookie( - &self, - path: &str, - payload: &T, - token: Option<&str>, - cookie: Option<&str>, - ) -> Result> { - let body = serde_json::to_vec(payload)?; - let mut builder = Request::builder() - .method(Method::POST) - .uri(path) - .header("content-type", "application/json"); - if let Some(token) = token { - builder = builder.header("authorization", format!("Bearer {token}")); - } - if let Some(cookie) = cookie { - builder = builder.header(header::COOKIE, cookie); - } - let request = builder.body(Body::from(body))?; - Ok(self - .router - .clone() - .oneshot(request) - .await - .expect("infallible response")) - } - - #[allow(dead_code)] - pub async fn patch_json( - &self, - path: &str, - payload: &T, - token: Option<&str>, - ) -> Result> { - let body = serde_json::to_vec(payload)?; - let mut builder = Request::builder() - .method(Method::PATCH) - .uri(path) - .header("content-type", "application/json"); - if let Some(token) = token { - builder = builder.header("authorization", format!("Bearer {token}")); - } - let request = builder.body(Body::from(body))?; - Ok(self - .router - .clone() - .oneshot(request) - .await - .expect("infallible response")) - } - - pub async fn get(&self, path: &str, token: Option<&str>) -> Result> { - let mut builder = Request::builder().method(Method::GET).uri(path); - if let Some(token) = token { - builder = builder.header("authorization", format!("Bearer {token}")); - } - let request = builder.body(Body::empty())?; - Ok(self - .router - .clone() - .oneshot(request) - .await - .expect("infallible response")) - } - - #[allow(dead_code)] - pub async fn delete(&self, path: &str, token: Option<&str>) -> Result> { - let builder = Request::builder().method(Method::DELETE).uri(path); - let builder = if let Some(token) = token { - builder.header("authorization", format!("Bearer {token}")) - } else { - builder - }; - let request = builder.body(Body::empty())?; - Ok(self - .router - .clone() - .oneshot(request) - .await - .expect("infallible response")) - } - - #[allow(dead_code)] - pub async fn upload_document( - &self, - path: &str, - filename: &str, - content_type: &str, - data: &[u8], - folder_id: Option, - token: &str, - ) -> Result> { - let extras = UploadExtras::empty(); - self.upload_document_with_extras( - path, - filename, - content_type, - data, - folder_id, - extras, - token, - ) - .await - } - - pub async fn upload_document_with_options( - &self, - path: &str, - filename: &str, - content_type: &str, - data: &[u8], - folder_id: Option, - title: Option<&str>, - metadata_json: Option<&str>, - token: &str, - ) -> Result> { - let extras = UploadExtras { - title, - metadata_json, - tag_ids_json: None, - correspondents_json: None, - issued_at: None, - skip_existing: None, - }; - self.upload_document_with_extras( - path, - filename, - content_type, - data, - folder_id, - extras, - token, - ) - .await - } - - pub async fn upload_document_with_extras( - &self, - path: &str, - filename: &str, - content_type: &str, - data: &[u8], - folder_id: Option, - extras: UploadExtras<'_>, - token: &str, - ) -> Result> { - let boundary = format!("boundary-{}", Uuid::new_v4()); - let mut body = Vec::new(); - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend( - format!( - "Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n", - filename - ) - .as_bytes(), - ); - body.extend(format!("Content-Type: {}\r\n\r\n", content_type).as_bytes()); - body.extend(data); - body.extend(b"\r\n"); - - if let Some(folder) = folder_id { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"folder_id\"\r\n\r\n"); - body.extend(folder.to_string().as_bytes()); - body.extend(b"\r\n"); - } - - if let Some(title_value) = extras.title { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"title\"\r\n\r\n"); - body.extend(title_value.as_bytes()); - body.extend(b"\r\n"); - } - - if let Some(metadata_value) = extras.metadata_json { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"metadata\"\r\n\r\n"); - body.extend(metadata_value.as_bytes()); - body.extend(b"\r\n"); - } - - if let Some(tag_ids_value) = extras.tag_ids_json { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"tag_ids\"\r\n\r\n"); - body.extend(tag_ids_value.as_bytes()); - body.extend(b"\r\n"); - } - - if let Some(correspondents_value) = extras.correspondents_json { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"correspondents\"\r\n\r\n"); - body.extend(correspondents_value.as_bytes()); - body.extend(b"\r\n"); - } - - if let Some(issued_at_value) = extras.issued_at { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"issued_at\"\r\n\r\n"); - body.extend(issued_at_value.as_bytes()); - body.extend(b"\r\n"); - } - - if let Some(skip_flag) = extras.skip_existing { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"skip_existing\"\r\n\r\n"); - body.extend(if skip_flag { - b"true".as_ref() - } else { - b"false".as_ref() - }); - body.extend(b"\r\n"); - } - - body.extend(format!("--{boundary}--\r\n").as_bytes()); - - let builder = Request::builder() - .method(Method::POST) - .uri(path) - .header( - "content-type", - format!("multipart/form-data; boundary={boundary}"), - ) - .header("authorization", format!("Bearer {token}")); - - let request = builder.body(Body::from(body))?; - Ok(self - .router - .clone() - .oneshot(request) - .await - .expect("infallible response")) - } - - pub async fn with_conn(&self, f: F) -> Result - where - F: FnOnce(&mut PgConnection) -> Result + Send + 'static, - T: Send + 'static, - { - let pool = self.state.pool.clone(); - tokio::task::spawn_blocking(move || { - let mut conn = pool - .get() - .map_err(|err| anyhow!("failed to get database connection: {err}"))?; - f(&mut conn) - }) - .await - .context("connection task panicked")? - } -} - -pub struct UploadExtras<'a> { - pub title: Option<&'a str>, - pub metadata_json: Option<&'a str>, - pub tag_ids_json: Option<&'a str>, - pub correspondents_json: Option<&'a str>, - pub issued_at: Option<&'a str>, - pub skip_existing: Option, -} - -impl<'a> UploadExtras<'a> { - pub fn empty() -> Self { - Self { - title: None, - metadata_json: None, - tag_ids_json: None, - correspondents_json: None, - issued_at: None, - skip_existing: None, - } - } -} - -pub async fn acquire_db_lock() -> tokio::sync::MutexGuard<'static, ()> { - DB_LOCK.lock().await -} - -pub async fn body_to_vec(body: Body) -> Result> { - let collected = body - .collect() - .await - .map_err(|err| anyhow!("failed to read response body: {err}"))?; - Ok(collected.to_bytes().to_vec()) -} - -#[cfg(test)] -mod helper_tests { - use super::*; - - #[tokio::test] - async fn create_session_and_login_token_provide_access() -> Result<()> { - let _lock = acquire_db_lock().await; - let app = TestApp::new().await?; - - let username = "helper-login"; - let password = "irrelevant"; - app.insert_user(username, TestUserRole::Owner).await?; - - let (access, refresh, refresh_id) = app.create_session(username).await?; - assert!(!access.is_empty(), "access token should not be empty"); - assert!(!refresh.is_empty(), "refresh token should not be empty"); - assert_ne!( - refresh_id, - Uuid::nil(), - "refresh token id should be assigned" - ); - - let bearer = app.login_token(username, password).await?; - assert!(!bearer.is_empty(), "login_token must yield bearer"); - - app.cleanup().await?; - Ok(()) - } - - #[tokio::test] - async fn insert_passkey_and_upload_with_options_succeeds() -> Result<()> { - let _lock = acquire_db_lock().await; - let app = TestApp::new().await?; - let username = "helper-passkey"; - let password = "unused"; - let user_id = app.insert_user(username, TestUserRole::Owner).await?; - - let passkey_id = app.insert_passkey(user_id, Some("Laptop")).await?; - assert_ne!(passkey_id, Uuid::nil()); - - let bearer = app.login_token(username, password).await?; - let response = app - .upload_document_with_options( - "/api/documents", - "helper.txt", - "text/plain", - b"helper-content", - None, - Some("Helper Note"), - Some("{\"category\":\"note\"}"), - &bearer, - ) - .await?; - assert!(response.status().is_success()); - - app.cleanup().await?; - Ok(()) - } -} - -async fn prepare_database(pool: &PgPool) -> Result<()> { - let pool = pool.clone(); - tokio::task::spawn_blocking(move || -> Result<()> { - let mut conn = pool - .get() - .map_err(|err| anyhow!("failed to acquire connection: {err}"))?; - conn.batch_execute(RESET_DATABASE_SQL) - .map_err(|err| anyhow!("failed to reset schema: {err}"))?; - conn.batch_execute("DROP TABLE IF EXISTS __diesel_schema_migrations;") - .map_err(|err| anyhow!("failed to drop diesel schema table: {err}"))?; - conn.run_pending_migrations(MIGRATIONS) - .map_err(|err| anyhow!("failed to run migrations: {err}"))?; - truncate_all(&mut conn)?; - Ok(()) - }) - .await - .context("migration task panicked")? -} - -fn truncate_all(conn: &mut PgConnection) -> Result<()> { - conn.batch_execute( - "TRUNCATE TABLE \ - tenant.document_asset_objects, \ - tenant.document_assets, \ - tenant.document_correspondents, \ - tenant.correspondents, \ - tenant.document_tags, \ - tenant.document_versions, \ - tenant.documents, \ - tenant.folders, \ - shared.jobs, \ - tenant.user_sessions, \ - tenant.tags, \ - tenant.api_tokens, \ - shared.webauthn_challenges, \ - shared.user_passkeys, \ - tenant.user_memberships, \ - shared.users, \ - shared.magic_tokens, \ - shared.tenants \ - RESTART IDENTITY CASCADE;", - ) - .context("failed to truncate tables")?; - Ok(()) -} - -fn generate_session_token() -> String { - let mut bytes = [0u8; 32]; - OsRng.fill_bytes(&mut bytes); - hex::encode(bytes) -} - -fn hash_session_token(value: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(value.as_bytes()); - hex::encode(hasher.finalize()) -} +pub use papercrate::test_support::*; diff --git a/backend/tests/correspondents_flow.rs b/backend/tests/correspondents_flow.rs index e297ccc..ff057d7 100644 --- a/backend/tests/correspondents_flow.rs +++ b/backend/tests/correspondents_flow.rs @@ -1,7 +1,5 @@ -mod common; - use anyhow::Result; -use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use serde::Deserialize; use serde_json::json; use uuid::Uuid; diff --git a/backend/tests/documents_flow.rs b/backend/tests/documents_flow.rs index 0cacb09..1fb78cd 100644 --- a/backend/tests/documents_flow.rs +++ b/backend/tests/documents_flow.rs @@ -1,9 +1,7 @@ -mod common; - use anyhow::{anyhow, Result}; use axum::http::StatusCode; -use common::{acquire_db_lock, body_to_vec, TestApp, UploadExtras, TestUserRole}; use diesel::prelude::*; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole, UploadExtras}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use uuid::Uuid; @@ -1823,7 +1821,8 @@ async fn delete_document_requires_trash() -> Result<()> { let app = TestApp::new().await?; let password = "conflict"; - app.insert_user("conflict-user", TestUserRole::Owner).await?; + app.insert_user("conflict-user", TestUserRole::Owner) + .await?; let token = app.login_token("conflict-user", password).await?; let upload = app diff --git a/backend/tests/download_flow.rs b/backend/tests/download_flow.rs index 42b1830..e60a0c0 100644 --- a/backend/tests/download_flow.rs +++ b/backend/tests/download_flow.rs @@ -1,8 +1,6 @@ -mod common; - use anyhow::Result; use axum::http::StatusCode; -use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use serde::Deserialize; #[derive(Deserialize)] diff --git a/backend/tests/folders_flow.rs b/backend/tests/folders_flow.rs index cf49516..a6808ae 100644 --- a/backend/tests/folders_flow.rs +++ b/backend/tests/folders_flow.rs @@ -1,8 +1,6 @@ -mod common; - use anyhow::Result; use axum::http::StatusCode; -use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use serde::Deserialize; use serde::Serialize; use serde_json::json; diff --git a/backend/tests/tags_flow.rs b/backend/tests/tags_flow.rs index 1fe2550..2103072 100644 --- a/backend/tests/tags_flow.rs +++ b/backend/tests/tags_flow.rs @@ -1,8 +1,5 @@ -mod common; - use anyhow::{anyhow, Result}; use axum::http::StatusCode; -use common::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use diesel::prelude::*; use papercrate::auth::capability_sets::{ensure_capability_set, owner_capabilities}; use papercrate::models::{NewUser, NewUserMembership, Tag, TenantStatus}; @@ -10,6 +7,7 @@ use papercrate::schema::{ tags::dsl as tags_dsl, tenants::dsl as tenants_dsl, user_memberships::dsl as memberships_dsl, users::dsl as users_dsl, }; +use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole}; use serde::Deserialize; use serde::Serialize; use uuid::Uuid;