diff --git a/.sqlx/query-46cced5ef72ee42127c3e892f22b7bac0f5e82ea382d422115ab5d52660b8526.json b/.sqlx/query-46cced5ef72ee42127c3e892f22b7bac0f5e82ea382d422115ab5d52660b8526.json new file mode 100644 index 0000000..15faa66 --- /dev/null +++ b/.sqlx/query-46cced5ef72ee42127c3e892f22b7bac0f5e82ea382d422115ab5d52660b8526.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT token FROM user_permissions WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "46cced5ef72ee42127c3e892f22b7bac0f5e82ea382d422115ab5d52660b8526" +} diff --git a/.sqlx/query-606364c79e0990deb07dfbe6c32b3d302d083ec5333f3a5ce04113c38a041100.json b/.sqlx/query-606364c79e0990deb07dfbe6c32b3d302d083ec5333f3a5ce04113c38a041100.json index 1748a62..59465cc 100644 --- a/.sqlx/query-606364c79e0990deb07dfbe6c32b3d302d083ec5333f3a5ce04113c38a041100.json +++ b/.sqlx/query-606364c79e0990deb07dfbe6c32b3d302d083ec5333f3a5ce04113c38a041100.json @@ -10,16 +10,21 @@ }, { "ordinal": 1, + "name": "anonymous", + "type_info": "Bool" + }, + { + "ordinal": 2, "name": "username", "type_info": "Text" }, { - "ordinal": 2, + "ordinal": 3, "name": "password", "type_info": "Text" }, { - "ordinal": 3, + "ordinal": 4, "name": "created_at", "type_info": "Timestamptz" } @@ -33,6 +38,7 @@ false, false, false, + false, true ] }, diff --git a/.sqlx/query-7b9e290b573c60081882ba3484e48d91b0630f9f36ef4ea01a58ef9af37a4461.json b/.sqlx/query-7b9e290b573c60081882ba3484e48d91b0630f9f36ef4ea01a58ef9af37a4461.json new file mode 100644 index 0000000..6bf1297 --- /dev/null +++ b/.sqlx/query-7b9e290b573c60081882ba3484e48d91b0630f9f36ef4ea01a58ef9af37a4461.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_permissions (user_id, token) VALUES ($1, $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [] + }, + "hash": "7b9e290b573c60081882ba3484e48d91b0630f9f36ef4ea01a58ef9af37a4461" +} diff --git a/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json b/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json index 68b8901..359fddc 100644 --- a/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json +++ b/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json @@ -10,16 +10,21 @@ }, { "ordinal": 1, + "name": "anonymous", + "type_info": "Bool" + }, + { + "ordinal": 2, "name": "username", "type_info": "Text" }, { - "ordinal": 2, + "ordinal": 3, "name": "password", "type_info": "Text" }, { - "ordinal": 3, + "ordinal": 4, "name": "created_at", "type_info": "Timestamptz" } @@ -33,6 +38,7 @@ false, false, false, + false, true ] }, diff --git a/.sqlx/query-26e7e05427bc7dabcd7815d27764fda2baf4cfe60a2d2d6ee2a1f773dccbbce2.json b/.sqlx/query-bdc63a2b6621293d4559832fb28a5bd3eb316ce9fc4902e985c6ddf88850ac86.json similarity index 71% rename from .sqlx/query-26e7e05427bc7dabcd7815d27764fda2baf4cfe60a2d2d6ee2a1f773dccbbce2.json rename to .sqlx/query-bdc63a2b6621293d4559832fb28a5bd3eb316ce9fc4902e985c6ddf88850ac86.json index 7410235..f83a51f 100644 --- a/.sqlx/query-26e7e05427bc7dabcd7815d27764fda2baf4cfe60a2d2d6ee2a1f773dccbbce2.json +++ b/.sqlx/query-bdc63a2b6621293d4559832fb28a5bd3eb316ce9fc4902e985c6ddf88850ac86.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM users", + "query": "SELECT * FROM users WHERE id <> 1", "describe": { "columns": [ { @@ -10,16 +10,21 @@ }, { "ordinal": 1, + "name": "anonymous", + "type_info": "Bool" + }, + { + "ordinal": 2, "name": "username", "type_info": "Text" }, { - "ordinal": 2, + "ordinal": 3, "name": "password", "type_info": "Text" }, { - "ordinal": 3, + "ordinal": 4, "name": "created_at", "type_info": "Timestamptz" } @@ -31,8 +36,9 @@ false, false, false, + false, true ] }, - "hash": "26e7e05427bc7dabcd7815d27764fda2baf4cfe60a2d2d6ee2a1f773dccbbce2" + "hash": "bdc63a2b6621293d4559832fb28a5bd3eb316ce9fc4902e985c6ddf88850ac86" } diff --git a/.sqlx/query-c0f27e55a1d15bf5d50cf692ed38e3299fab99554ce0dcfc6aa2791a3b56acb9.json b/.sqlx/query-c0f27e55a1d15bf5d50cf692ed38e3299fab99554ce0dcfc6aa2791a3b56acb9.json new file mode 100644 index 0000000..7f1cba1 --- /dev/null +++ b/.sqlx/query-c0f27e55a1d15bf5d50cf692ed38e3299fab99554ce0dcfc6aa2791a3b56acb9.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM user_permissions", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "token", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "c0f27e55a1d15bf5d50cf692ed38e3299fab99554ce0dcfc6aa2791a3b56acb9" +} diff --git a/.sqlx/query-1ea4a3f1d8f72d6aa47e63a6b10518dd2961b8f6e72189105959ad8cd065702f.json b/.sqlx/query-fa949e4cabae53f3ef84b563899517e41415c523f00a1e6c6bd0489a54a347d5.json similarity index 56% rename from .sqlx/query-1ea4a3f1d8f72d6aa47e63a6b10518dd2961b8f6e72189105959ad8cd065702f.json rename to .sqlx/query-fa949e4cabae53f3ef84b563899517e41415c523f00a1e6c6bd0489a54a347d5.json index b33f5bc..fa0d658 100644 --- a/.sqlx/query-1ea4a3f1d8f72d6aa47e63a6b10518dd2961b8f6e72189105959ad8cd065702f.json +++ b/.sqlx/query-fa949e4cabae53f3ef84b563899517e41415c523f00a1e6c6bd0489a54a347d5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id", + "query": "INSERT INTO users (username, password, anonymous) VALUES ($1, $2, $3) RETURNING id", "describe": { "columns": [ { @@ -12,12 +12,13 @@ "parameters": { "Left": [ "Text", - "Text" + "Text", + "Bool" ] }, "nullable": [ false ] }, - "hash": "1ea4a3f1d8f72d6aa47e63a6b10518dd2961b8f6e72189105959ad8cd065702f" + "hash": "fa949e4cabae53f3ef84b563899517e41415c523f00a1e6c6bd0489a54a347d5" } diff --git a/Cargo.lock b/Cargo.lock index 1237214..6d1f257 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -147,7 +182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.10.0", + "bitflags 2.11.0", "cc", "cesu8", "jni", @@ -187,9 +222,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "anymap3" @@ -268,9 +303,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -293,7 +328,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -324,7 +359,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -350,7 +385,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -444,6 +479,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.8" @@ -500,6 +557,73 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum_session" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7815c0f6c12768f5f1fb1f1340d21c8b3ce9139bb0b8eb07a211b30f85fa1d50" +dependencies = [ + "aes-gcm", + "async-trait", + "axum", + "base64", + "bytes", + "chrono", + "cookie", + "dashmap", + "forwarded-header-value", + "futures", + "hmac", + "http", + "http-body", + "rand 0.9.2", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "axum_session_auth" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ce1f03d6a0649a4ecb34a26f044a2d0bc0a38afcb5b413de1346549e835132" +dependencies = [ + "anyhow", + "async-recursion", + "async-trait", + "axum-core", + "axum_session", + "bytes", + "chrono", + "dashmap", + "futures", + "http", + "http-body", + "serde", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum_session_sqlx" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5b7fde26383cda9cecce602d38d2c92351274a70c1f23570fb09b7acd6c159" +dependencies = [ + "async-trait", + "axum_session", + "chrono", + "sqlx", +] + [[package]] name = "base64" version = "0.22.1" @@ -512,15 +636,30 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -535,9 +674,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -602,15 +741,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -640,9 +779,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calloop" @@ -650,7 +789,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "log", "polling", "rustix 0.38.44", @@ -660,13 +799,13 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "tracing", ] @@ -689,17 +828,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ - "calloop 0.14.3", - "rustix 1.1.3", + "calloop 0.14.4", + "rustix 1.1.4", "wayland-backend", "wayland-client", ] [[package]] name = "cc" -version = "1.2.54" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -739,6 +878,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -748,6 +897,15 @@ dependencies = [ "error-code", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.12.0" @@ -802,9 +960,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-str" -version = "0.7.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160" +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" [[package]] name = "const_format" @@ -828,13 +986,46 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "aes-gcm", + "base64", + "percent-encoding", + "rand 0.8.5", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "copypasta" version = "0.10.2" @@ -905,7 +1096,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "libc", ] @@ -980,9 +1171,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -995,8 +1196,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1013,13 +1224,38 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn", ] @@ -1057,13 +1293,44 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -1087,11 +1354,11 @@ dependencies = [ [[package]] name = "derive_setters" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" +checksum = "b7e6f6fa1f03c14ae082120b84b3c7fbd7b8588d924cf2d7c3daf9afd49df8b9" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn", @@ -1109,6 +1376,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1162,6 +1450,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -1270,6 +1564,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set 0.5.3", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1287,15 +1592,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1407,6 +1712,22 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror 1.0.69", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -1415,9 +1736,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1430,9 +1751,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1440,15 +1761,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1468,9 +1789,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1487,9 +1808,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1498,21 +1819,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1522,7 +1843,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1542,7 +1862,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link 0.2.1", ] @@ -1553,8 +1873,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1564,9 +1886,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", ] [[package]] @@ -1597,6 +1934,16 @@ dependencies = [ "syn", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1635,7 +1982,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-alloc-types", ] @@ -1645,7 +1992,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -1666,7 +2013,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -1677,7 +2024,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -1690,6 +2037,25 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -1708,7 +2074,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "core_maths", "read-fonts 0.35.0", @@ -1741,6 +2107,8 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -1858,6 +2226,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1870,16 +2239,31 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1889,16 +2273,18 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2000,6 +2386,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2037,7 +2429,7 @@ dependencies = [ "byteorder-lite", "moxcms", "num-traits", - "png 0.18.0", + "png 0.18.1", ] [[package]] @@ -2052,11 +2444,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" dependencies = [ "rustversion", ] @@ -2077,6 +2478,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2117,9 +2527,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" dependencies = [ "once_cell", "wasm-bindgen", @@ -2131,7 +2541,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", ] @@ -2156,8 +2566,14 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" name = "kreqo-core" version = "0.0.0" dependencies = [ + "anyhow", "argon2", + "async-trait", + "axum_session", + "axum_session_auth", "chrono", + "directories", + "hashbrown 0.16.1", "serde", "server_fn", "sqlx", @@ -2169,6 +2585,7 @@ name = "kreqo-learn" version = "0.0.0" dependencies = [ "kreqo-core", + "kreqo-server", "kreqo-ui", "server_fn", "xilem", @@ -2181,13 +2598,22 @@ dependencies = [ "anyhow", "argon2", "axum", + "axum_session", + "axum_session_auth", + "axum_session_sqlx", + "bytes", "cfg-if", + "cookie_store", "dotenvy", + "futures", "kreqo-core", + "reqwest", + "reqwest_cookie_store", "server_fn", "server_fn_macro_default 0.0.0", "sqlx", "tokio", + "tokio-tungstenite", "tracing", "tracing-subscriber", ] @@ -2198,11 +2624,13 @@ version = "0.0.0" dependencies = [ "kreqo-core", "kreqo-server", + "parley", "rapidfuzz", "server_fn", "thiserror 2.0.18", "uuid", "xilem", + "zxcvbn", ] [[package]] @@ -2225,26 +2653,32 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libdeflate-sys" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bd6304ebf75390d8a99b88bdf2a266f62647838140cb64af8e6702f6e3fddc" +checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a" dependencies = [ "cc", ] [[package]] name = "libdeflater" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d4880e6d634d3d029d65fa016038e788cc728a17b782684726fb34ee140caf" +checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71" dependencies = [ "libdeflate-sys", ] @@ -2271,9 +2705,9 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.0", + "redox_syscall 0.7.1", ] [[package]] @@ -2305,11 +2739,10 @@ checksum = "834e592123b39b7b3ba3fdc4b7e4822fad3ced449010f8229f843fe6dd1a33f1" [[package]] name = "linebender_include_doc_path" version = "0.1.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "proc-macro2", "quote", - "syn", ] [[package]] @@ -2326,9 +2759,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2357,6 +2790,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2369,7 +2808,7 @@ dependencies = [ [[package]] name = "masonry" version = "0.4.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "accesskit", "dpi", @@ -2377,6 +2816,7 @@ dependencies = [ "masonry_core", "masonry_testing", "parley", + "smallvec", "tracing", "vello", ] @@ -2384,7 +2824,7 @@ dependencies = [ [[package]] name = "masonry_core" version = "0.4.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "accesskit", "anymap3", @@ -2409,7 +2849,7 @@ dependencies = [ [[package]] name = "masonry_testing" version = "0.4.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "accesskit_consumer", "futures-intrusive", @@ -2423,7 +2863,7 @@ dependencies = [ [[package]] name = "masonry_winit" version = "0.4.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "accesskit_winit", "copypasta", @@ -2461,15 +2901,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -2511,7 +2951,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "core-graphics-types 0.2.0", "foreign-types", @@ -2591,8 +3031,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" dependencies = [ "arrayvec", - "bit-set", - "bitflags 2.10.0", + "bit-set 0.8.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -2616,7 +3056,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys", @@ -2640,6 +3080,12 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2763,7 +3209,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2 0.5.2", @@ -2779,7 +3225,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -2803,7 +3249,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2815,7 +3261,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2848,7 +3294,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2-core-foundation", ] @@ -2864,7 +3310,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "dispatch", "libc", @@ -2877,7 +3323,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2 0.6.3", ] @@ -2899,7 +3345,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2911,7 +3357,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2934,7 +3380,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2 0.5.2", "objc2-cloud-kit", @@ -2966,7 +3412,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -2979,6 +3425,29 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "git+https://github.com/darakreqo/leptos#7c27903d2a13f2814f004b55a15639b44dc1b5f9" + [[package]] name = "orbclient" version = "0.3.50" @@ -3204,11 +3673,11 @@ dependencies = [ [[package]] name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -3225,7 +3694,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -3235,17 +3704,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -3280,6 +3761,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -3326,6 +3817,22 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "pxfm" version = "0.1.27" @@ -3345,6 +3852,62 @@ dependencies = [ "serde", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -3479,23 +4042,46 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "bitflags 2.10.0", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3504,9 +4090,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "renderdoc-sys" @@ -3516,29 +4102,37 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", + "cookie", + "cookie_store", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", + "mime", "mime_guess", "percent-encoding", "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3550,6 +4144,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest_cookie_store" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb98c4d52ae6ceed18a0dff0564717623dd75fae08619ae0927332f0a81abbc" +dependencies = [ + "bytes", + "cookie_store", + "reqwest", + "url", +] + [[package]] name = "rgb" version = "0.8.52" @@ -3629,7 +4235,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3638,14 +4244,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3655,6 +4261,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -3663,21 +4270,62 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3691,9 +4339,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3704,6 +4352,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3729,6 +4386,29 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -3840,23 +4520,21 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353d02fa2886cd8dae0b8da0965289fa8f2ecc7df633d1ce965f62fdf9644d29" +version = "0.8.10" +source = "git+https://github.com/darakreqo/leptos#7c27903d2a13f2814f004b55a15639b44dc1b5f9" dependencies = [ "axum", "base64", "bytes", "const-str", "const_format", - "dashmap", "futures", "http", "http-body-util", "hyper", "inventory", + "or_poisoned", "pin-project-lite", - "reqwest", "rustc_version", "rustversion", "serde", @@ -3866,7 +4544,6 @@ dependencies = [ "thiserror 2.0.18", "throw_error", "tokio", - "tokio-tungstenite", "tower", "tower-layer", "url", @@ -3875,9 +4552,8 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950b8cfc9ff5f39ca879c5a7c5e640de2695a199e18e424c3289d0964cabe642" +version = "0.8.9" +source = "git+https://github.com/darakreqo/leptos#7c27903d2a13f2814f004b55a15639b44dc1b5f9" dependencies = [ "const_format", "convert_case", @@ -3899,8 +4575,7 @@ dependencies = [ [[package]] name = "server_fn_macro_default" version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00" +source = "git+https://github.com/darakreqo/leptos#7c27903d2a13f2814f004b55a15639b44dc1b5f9" dependencies = [ "server_fn_macro", "syn", @@ -3991,9 +4666,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -4019,7 +4694,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -4044,14 +4719,14 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.10.0", - "calloop 0.14.3", + "bitflags 2.11.0", + "calloop 0.14.4", "calloop-wayland-source 0.4.1", "cursor-icon", "libc", "log", "memmap2", - "rustix 1.1.3", + "rustix 1.1.4", "thiserror 2.0.18", "wayland-backend", "wayland-client", @@ -4110,7 +4785,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -4170,6 +4845,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", "webpki-roots 0.26.11", ] @@ -4219,7 +4895,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "bytes", "chrono", @@ -4251,6 +4927,7 @@ dependencies = [ "stringprep", "thiserror 2.0.18", "tracing", + "uuid", "whoami", ] @@ -4262,7 +4939,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "chrono", "crc", @@ -4289,6 +4966,7 @@ dependencies = [ "stringprep", "thiserror 2.0.18", "tracing", + "uuid", "whoami", ] @@ -4315,6 +4993,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "url", + "uuid", ] [[package]] @@ -4389,9 +5068,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4418,6 +5097,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -4426,14 +5126,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4451,6 +5151,7 @@ name = "tests" version = "0.0.0" dependencies = [ "gh-workflow", + "kreqo-core", "serde_json", ] @@ -4506,17 +5207,16 @@ dependencies = [ [[package]] name = "throw_error" version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64" +source = "git+https://github.com/darakreqo/leptos#7c27903d2a13f2814f004b55a15639b44dc1b5f9" dependencies = [ "pin-project-lite", ] [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -4535,9 +5235,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4622,6 +5322,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -4681,9 +5391,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -4710,7 +5420,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -4825,7 +5535,7 @@ dependencies = [ [[package]] name = "tree_arena" version = "0.2.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "hashbrown 0.16.1", ] @@ -4912,9 +5622,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -4949,6 +5659,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4981,11 +5701,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -5088,6 +5808,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -5096,9 +5825,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" dependencies = [ "cfg-if", "once_cell", @@ -5109,9 +5838,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" dependencies = [ "cfg-if", "futures-util", @@ -5123,9 +5852,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5133,9 +5862,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" dependencies = [ "bumpalo", "proc-macro2", @@ -5146,18 +5875,40 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -5166,6 +5917,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.12" @@ -5174,7 +5937,7 @@ checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.3", + "rustix 1.1.4", "scoped-tls", "smallvec", "wayland-sys", @@ -5186,8 +5949,8 @@ version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.3", + "bitflags 2.11.0", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] @@ -5198,7 +5961,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cursor-icon", "wayland-backend", ] @@ -5209,7 +5972,7 @@ version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "wayland-client", "xcursor", ] @@ -5220,7 +5983,7 @@ version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5232,7 +5995,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5245,7 +6008,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5258,7 +6021,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5271,7 +6034,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5303,9 +6066,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" dependencies = [ "js-sys", "wasm-bindgen", @@ -5321,20 +6084,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -5346,7 +6118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "document-features", @@ -5375,9 +6147,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" dependencies = [ "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.10.0", + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.11.0", "bytemuck", "cfg_aliases", "document-features", @@ -5436,8 +6208,8 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", - "bitflags 2.10.0", + "bit-set 0.8.0", + "bitflags 2.11.0", "block", "bytemuck", "cfg-if", @@ -5482,7 +6254,7 @@ version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "js-sys", "log", @@ -5679,6 +6451,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -6049,7 +6832,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "bytemuck", "calloop 0.13.0", @@ -6106,6 +6889,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -6154,7 +7019,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -6173,7 +7038,7 @@ checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" [[package]] name = "xilem" version = "0.4.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "masonry", "masonry_winit", @@ -6188,7 +7053,7 @@ dependencies = [ [[package]] name = "xilem_core" version = "0.4.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "anymore", "hashbrown 0.16.1", @@ -6198,7 +7063,7 @@ dependencies = [ [[package]] name = "xilem_masonry" version = "0.4.0" -source = "git+https://github.com/linebender/xilem#d705a04eb9162879be5fdfc64280ded446e7aaff" +source = "git+https://github.com/linebender/xilem#404bb27a2481ff66e37d20608a55676154d3485b" dependencies = [ "masonry", "tokio", @@ -6213,7 +7078,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dlib", "log", "once_cell", @@ -6280,9 +7145,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.13.2" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", @@ -6300,7 +7165,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.3", + "rustix 1.1.4", "serde", "serde_repr", "tracing", @@ -6339,9 +7204,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.13.2" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6383,18 +7248,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -6464,15 +7329,15 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "5.9.2" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", @@ -6484,9 +7349,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.9.2" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6507,3 +7372,20 @@ dependencies = [ "syn", "winnow", ] + +[[package]] +name = "zxcvbn" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad76e35b00ad53688d6b90c431cabe3cbf51f7a4a154739e04b63004ab1c736c" +dependencies = [ + "chrono", + "derive_builder", + "fancy-regex", + "itertools", + "lazy_static", + "regex", + "time", + "wasm-bindgen", + "web-sys", +] diff --git a/Cargo.toml b/Cargo.toml index 2281b3a..bb00f06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,23 +8,37 @@ edition = "2024" [workspace.dependencies] anyhow = "1.0" argon2 = "0.5" +async-trait = "0.1" axum = "0.8" +axum_session = "0.18" +axum_session_auth = "0.18" +axum_session_sqlx = "0.7" +bytes = "1.11" cfg-if = "1.0" chrono = { version = "0.4", features = ["serde"] } +cookie_store = "0.22" +directories = "6.0" dotenvy = "0.15" +futures = "0.3" +hashbrown = { version = "0.16", features = ["serde"] } +parley = "0.7" rapidfuzz = "0.5" +reqwest = { version = "0.13", features = ["multipart", "stream", "cookies"] } +reqwest_cookie_store = "0.10" rs-fsrs = "1.2" serde = { version = "1.0", features = ["derive"] } -server_fn = { version = "0.8", default-features = false, features = ["axum", "reqwest"] } -server_fn_macro = { version = "0.8", default-features = false, features = ["axum", "reqwest"] } +server_fn = { version = "0.8", default-features = false, features = ["axum"] } +server_fn_macro = { version = "0.8", default-features = false, features = ["axum"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "chrono"] } syn = "2.0" thiserror = "2.0" tokio = { version = "1.48", features = ["full"] } +tokio-tungstenite = "0.28" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.20", features = ["v4"] } xilem = { git = "https://github.com/linebender/xilem" } +zxcvbn = "3.1" kreqo-core = { path = "./core" } kreqo-server = { path = "./server", default-features = false } @@ -35,3 +49,5 @@ kreqo-learn = { path = "./apps/learn" } [patch.crates-io] gh-workflow = { git = "https://github.com/darakreqo/gh-workflow" } +server_fn = { git = "https://github.com/darakreqo/leptos" } +server_fn_macro = { git = "https://github.com/darakreqo/leptos" } diff --git a/apps/learn/Cargo.toml b/apps/learn/Cargo.toml index a15c4f0..6e51d64 100644 --- a/apps/learn/Cargo.toml +++ b/apps/learn/Cargo.toml @@ -8,4 +8,5 @@ server_fn.workspace = true xilem.workspace = true kreqo-core.workspace = true +kreqo-server.workspace = true kreqo-ui.workspace = true diff --git a/apps/learn/src/lib.rs b/apps/learn/src/lib.rs index 41f50bd..04e97ac 100644 --- a/apps/learn/src/lib.rs +++ b/apps/learn/src/lib.rs @@ -1,16 +1,38 @@ -use kreqo_core::User; -use kreqo_ui::component::AsyncList; +use kreqo_core::users::User; +use kreqo_server::api::{cleanup_expired_sessions, current_user, login, logout}; +use kreqo_server::custom_client::request::save_cookies; +use kreqo_ui::auth_forms::{AuthMessage, AuthRequest, UserLoginForm}; +use kreqo_ui::component::list::ListRequest; +use kreqo_ui::component::{AsyncList, Form, action_button, logo, user_profile_overview}; use kreqo_ui::theme::BACKGROUND_COLOR; use kreqo_ui::user_list::UserStorage; -use xilem::core::map_state; +use xilem::core::one_of::OneOf3; +use xilem::core::{fork, lens, map_action, map_state}; use xilem::masonry::layout::{AsUnit, Dim}; +use xilem::palette::css::GRAY; use xilem::style::Style; -use xilem::view::{FlexExt, MainAxisAlignment, flex_col, flex_row, portal, sized_box}; +use xilem::tokio::sync::mpsc::UnboundedSender; +use xilem::view::{ + FlexExt, MainAxisAlignment, flex_col, flex_row, label, portal, sized_box, split, text_button, + worker, +}; use xilem::{WindowId, WindowView, window}; +#[derive(Default)] +enum Page { + #[default] + Login, + Signup, + UserList, +} + pub struct AppState { running: bool, main_window_id: WindowId, + page: Page, + current_user: Option, + login_form: UserLoginForm, + auth_sender: Option>, user_list: AsyncList, } @@ -19,6 +41,10 @@ impl Default for AppState { Self { running: true, main_window_id: WindowId::next(), + page: Page::default(), + current_user: None, + login_form: UserLoginForm::default(), + auth_sender: None, user_list: AsyncList::new(true, true), } } @@ -32,24 +58,191 @@ impl xilem::AppState for AppState { impl AppState { pub fn logic(&mut self) -> impl Iterator> + use<> { - let user_list = flex_row(sized_box(self.user_list.view()).width(600.px())) - .main_axis_alignment(MainAxisAlignment::Center) - .width(Dim::Stretch) - .padding(15.); - let error = self.user_list.error_view().map(|error_view| { - flex_row(error_view) - .main_axis_alignment(MainAxisAlignment::Center) - .padding(15.) - }); - let portal = portal(user_list).flex(1.); - let content = map_state( - flex_col((portal, error)).gap(0.px()), - |state: &mut AppState, ()| &mut state.user_list, - ); + let page = match self.page { + Page::Login => { + let form = map_action( + lens(Form::view, move |state: &mut Self| &mut state.login_form), + |state: &mut Self, submit| { + state + .login_form + .handle_submit(submit, state.auth_sender.as_ref()); + }, + ); + let separator = label("OR").color(GRAY); + let goto_signup = + text_button("Sign Up", |state: &mut Self| state.page = Page::Signup) + .corner_radius(100.); + let content = flex_col(( + sized_box(form).dims((600.px(), Dim::MinContent)), + separator, + goto_signup, + )) + .main_axis_alignment(MainAxisAlignment::Center); + let worker = fork( + content, + worker( + |proxy, mut rx| async move { + while let Some(request) = rx.recv().await { + match request { + AuthRequest::Login(username, password) => { + if login(username, password).await.is_err() { + return; + } + let user = current_user().await.ok(); + drop(proxy.message(AuthMessage::UserRefreshed(user))); + } + AuthRequest::RefreshUser => { + let user = current_user().await.ok(); + drop(proxy.message(AuthMessage::UserRefreshed(user))); + } + _ => (), + } + } + }, + |state: &mut Self, sender| { + state.auth_sender = Some(sender); + state.auth_sender.as_ref().inspect(|sender| { + let _ = sender.send(AuthRequest::RefreshUser); + }); + }, + |state: &mut Self, message| { + if let AuthMessage::UserRefreshed(user) = message { + state.current_user = user.clone(); + if let Some(user) = user + && user.id != 1 + { + state.page = Page::UserList; + } + } + }, + ), + ); + OneOf3::A(worker) + } + Page::Signup => { + let form = map_action( + map_state( + AsyncList::worker(self.user_list.create_view()), + move |state: &mut Self| &mut state.user_list, + ), + |state: &mut Self, resolved| { + if matches!(resolved, Some(ListRequest::Create(_))) { + state.page = Page::Login; + } + }, + ); + let separator = label("OR").color(GRAY); + let goto_login = text_button("Log In", |state: &mut Self| { + state.page = Page::Login; + }) + .corner_radius(100.); + let content = flex_col(( + sized_box(form).dims((600.px(), Dim::MinContent)), + separator, + goto_login, + )) + .main_axis_alignment(MainAxisAlignment::Center); + OneOf3::B(content) + } + Page::UserList => { + let user_profile = self.current_user.as_ref().map(|_| { + lens(user_profile_overview, move |state: &mut Self| { + &mut state.current_user.as_mut().unwrap().username + }) + }); + let cleanup_sessions_button = self.current_user.as_ref().and_then(|user| { + user.permissions + .contains("Server::Manage") + .then_some(action_button("Cleanup sessions", |state: &mut Self| { + state.auth_sender.as_ref().inspect(|sender| { + let _ = sender.send(AuthRequest::CleanupSessions); + }); + })) + }); + let logout_button = action_button("Log Out", |state: &mut Self| { + state.auth_sender.as_ref().inspect(|sender| { + let _ = sender.send(AuthRequest::Logout); + }); + }); + let sidebar = + flex_col((logo(), user_profile, cleanup_sessions_button, logout_button)) + .gap(20.px()) + .padding(15.); + let sidebar_worker = fork( + sidebar, + worker( + |proxy, mut rx| async move { + while let Some(request) = rx.recv().await { + match request { + AuthRequest::CleanupSessions => { + if let Ok(removed_session_ids) = + cleanup_expired_sessions().await + { + println!( + "Successfully removed {} sessions", + removed_session_ids.len() + ); + drop(proxy.message(AuthMessage::SessionsCleanedUp)); + } + } + AuthRequest::Logout => { + if logout().await.is_ok() { + println!("Successfully logged out"); + drop(proxy.message(AuthMessage::UserRefreshed(None))); + } + } + _ => (), + } + } + }, + |state: &mut Self, sender| { + state.auth_sender = Some(sender); + }, + |state: &mut Self, message| { + if let AuthMessage::UserRefreshed(user) = message { + state.current_user = user.clone(); + if user.is_none() { + state.page = Page::default(); + } + } + }, + ), + ); + + let user_list = flex_row(sized_box(self.user_list.view()).width(600.px())) + .main_axis_alignment(MainAxisAlignment::Center) + .width(Dim::Stretch) + .padding(15.); + let user_list_error = self.user_list.error_view().map(|error_view| { + flex_row(error_view) + .main_axis_alignment(MainAxisAlignment::Center) + .padding(15.) + }); + let portal = portal(user_list).flex(1.); + let content = flex_col((portal, user_list_error)).gap(0.px()); + let worker = map_action( + map_state(AsyncList::worker(content), move |state: &mut Self| { + &mut state.user_list + }), + |_, _| (), + ); + + OneOf3::C( + split(sidebar_worker, worker) + .split_point_from_start(200.px()) + .draggable(false) + .solid_bar(true) + .bar_thickness(2.px()), + ) + } + }; std::iter::once( - window(self.main_window_id, "Kreqo Learn", content) + window(self.main_window_id, "Kreqo Learn", page) .with_options(|options| { - options.on_close(|state: &mut AppState| state.running = false) + options.on_close(|state: &mut AppState| { + state.running = false; + let _ = save_cookies(); + }) }) .with_base_color(BACKGROUND_COLOR), ) diff --git a/apps/learn/src/main.rs b/apps/learn/src/main.rs index 02f3614..0878c74 100644 --- a/apps/learn/src/main.rs +++ b/apps/learn/src/main.rs @@ -1,11 +1,12 @@ use kreqo_learn::AppState; +use kreqo_server::SERVER_ADDRESS; use kreqo_ui::theme::apply_theme; use xilem::masonry::theme::default_property_set; use xilem::winit::error::EventLoopError; use xilem::{EventLoop, Xilem}; fn main() -> Result<(), EventLoopError> { - server_fn::client::set_server_url("http://localhost:8080"); + server_fn::client::set_server_url(format!("http://{}", SERVER_ADDRESS).leak()); let mut def_props = default_property_set(); apply_theme(&mut def_props); diff --git a/core/Cargo.toml b/core/Cargo.toml index 04c9746..02c8d22 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -4,8 +4,14 @@ version = "0.0.0" edition.workspace = true [dependencies] +anyhow.workspace = true argon2.workspace = true +async-trait.workspace = true +axum_session.workspace = true +axum_session_auth.workspace = true chrono.workspace = true +directories.workspace = true +hashbrown.workspace = true serde.workspace = true server_fn.workspace = true sqlx.workspace = true diff --git a/core/src/database.rs b/core/src/database.rs new file mode 100644 index 0000000..61cc444 --- /dev/null +++ b/core/src/database.rs @@ -0,0 +1,197 @@ +use argon2::Argon2; +use argon2::password_hash::rand_core::OsRng; +use argon2::password_hash::{PasswordHasher, SaltString}; +use chrono::{DateTime, Utc}; +use hashbrown::{HashMap, HashSet}; +use sqlx::{FromRow, PgPool}; + +use crate::errors::ServerError; +use crate::users::User; +use crate::users::permissions::UserPermission; +use crate::users::roles::UserRole; + +#[derive(FromRow, Clone, Debug)] +pub struct SqlUser { + pub id: i64, + pub anonymous: bool, + pub username: String, + pub password: String, + pub created_at: Option>, +} + +impl SqlUser { + fn into_user(self, user_perms: Option>) -> User { + let permissions = if let Some(user_perms) = user_perms { + user_perms.into_iter().map(|perm| perm.token).collect() + } else { + HashSet::new() + }; + User { + id: self.id, + anonymous: self.anonymous, + username: self.username, + created_at: self.created_at, + permissions, + } + } + + pub async fn to_user(self, pool: &PgPool) -> Result { + let user_perms = get_user_perms(pool, self.id).await?; + Ok(self.into_user(Some(user_perms))) + } +} + +#[derive(FromRow)] +pub struct SqlUserPermission { + pub user_id: i64, + pub token: String, +} + +impl From for UserPermission { + fn from(val: SqlUserPermission) -> Self { + UserPermission { token: val.token } + } +} + +pub async fn get_user_perms( + pool: &PgPool, + user_id: i64, +) -> Result, ServerError> { + let user_perms = sqlx::query_as!( + UserPermission, + "SELECT token FROM user_permissions WHERE user_id = $1", + user_id + ) + .fetch_all(pool) + .await?; + Ok(user_perms) +} + +pub async fn add_user_perms( + pool: &PgPool, + user_id: i64, + user_perms: Vec, +) -> Result<(), ServerError> { + for perm in user_perms { + sqlx::query!( + "INSERT INTO user_permissions (user_id, token) VALUES ($1, $2)", + user_id, + perm.token + ) + .execute(pool) + .await?; + } + Ok(()) +} + +pub async fn get_sql_users(pool: &PgPool) -> Result, ServerError> { + Ok( + sqlx::query_as!(SqlUser, "SELECT * FROM users WHERE id <> 1") + .fetch_all(pool) + .await?, + ) +} + +pub async fn get_sql_user(pool: &PgPool, id: i64) -> Result { + Ok( + sqlx::query_as!(SqlUser, "SELECT * FROM users WHERE id = $1", id) + .fetch_one(pool) + .await?, + ) +} + +pub async fn get_sql_user_from_username( + pool: &PgPool, + username: String, +) -> Result { + Ok( + sqlx::query_as!(SqlUser, "SELECT * FROM users WHERE username = $1", username) + .fetch_one(pool) + .await?, + ) +} + +pub async fn get_users(pool: &PgPool) -> Result, ServerError> { + let sql_users = get_sql_users(pool).await?; + let mut perms_map = HashMap::with_capacity(sql_users.len()); + for sql_user_perm in sqlx::query_as!(SqlUserPermission, "SELECT * FROM user_permissions") + .fetch_all(pool) + .await? + { + let entry = perms_map.entry(sql_user_perm.user_id).or_insert(Vec::new()); + entry.push(sql_user_perm.into()); + } + Ok(sql_users + .iter() + .map(|sql_user| { + sql_user + .clone() + .into_user(perms_map.get(&sql_user.id).cloned()) + }) + .collect()) +} + +pub async fn get_user(pool: &PgPool, id: i64) -> Result { + get_sql_user(pool, id).await?.to_user(pool).await +} + +pub async fn get_user_from_username(pool: &PgPool, username: String) -> Result { + get_sql_user_from_username(pool, username) + .await? + .to_user(pool) + .await +} + +pub async fn create_user( + pool: &PgPool, + username: String, + password: String, + role: UserRole, +) -> Result { + let salt = SaltString::generate(&mut OsRng); + let password_hashed = Argon2::default() + .hash_password(password.as_bytes(), &salt)? + .to_string(); + + let id = sqlx::query_scalar!( + "INSERT INTO users (username, password, anonymous) VALUES ($1, $2, $3) RETURNING id", + username.clone(), + password_hashed, + false + ) + .fetch_one(pool) + .await?; + + add_user_perms(pool, id, role.permissions()).await?; + + // To check if the creation of the user was successfull + let user = get_user(pool, id).await?; + + Ok(user) +} + +pub async fn update_user_username( + pool: &PgPool, + id: i64, + username: String, +) -> Result { + let id = sqlx::query_scalar!( + "UPDATE users SET username = $2 WHERE id = $1 RETURNING id", + id, + username.clone(), + ) + .fetch_one(pool) + .await?; + + let user = get_user(pool, id).await?; + + Ok(user) +} + +pub async fn delete_user(pool: &PgPool, id: i64) -> Result { + Ok( + sqlx::query_scalar!("DELETE FROM users WHERE id = $1 RETURNING id", id) + .fetch_one(pool) + .await?, + ) +} diff --git a/core/src/errors.rs b/core/src/errors.rs index 6c70793..91c5ed7 100644 --- a/core/src/errors.rs +++ b/core/src/errors.rs @@ -1,4 +1,5 @@ use argon2::password_hash::Error as PasswordHashError; +use axum_session::SessionError; use serde::{Deserialize, Serialize}; use server_fn::codec::JsonEncoding; use server_fn::error::{FromServerFnError, ServerFnErrorErr}; @@ -8,12 +9,18 @@ use thiserror::Error; #[derive(Error, Debug, Serialize, Deserialize)] #[non_exhaustive] pub enum ServerError { - #[error("encountered an API error")] + #[error("API error: {0}")] API(ServerFnErrorErr), - #[error("encountered a database error")] + #[error("database error: {0}")] Database(String), - #[error("failed to hash password")] + #[error("session error: {0}")] + Session(String), + #[error("failed to hash password: {0}")] PasswordHash(String), + #[error("wrong username or password")] + WrongLogin, + #[error("authentication required or missing permissions")] + Unauthorized, } impl FromServerFnError for ServerError { @@ -30,6 +37,12 @@ impl From for ServerError { } } +impl From for ServerError { + fn from(value: SessionError) -> Self { + Self::Session(value.to_string()) + } +} + impl From for ServerError { fn from(value: PasswordHashError) -> Self { Self::PasswordHash(value.to_string()) diff --git a/core/src/lib.rs b/core/src/lib.rs index 791b693..f2f843f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,13 +1,36 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; +use std::path::PathBuf; +use std::sync::LazyLock; +use directories::ProjectDirs; + +pub mod database; pub mod errors; +pub mod users; + +pub static PROJECT_DIRS: LazyLock> = + LazyLock::new(|| ProjectDirs::from("org", "kreqo", "kreqo-learn")); -#[derive(FromRow, Debug, Clone, Default, Serialize, Deserialize)] -pub struct User { - pub id: i64, - pub username: String, - pub password: String, - pub created_at: Option>, +pub fn cookies_path() -> PathBuf { + let project_dirs = PROJECT_DIRS.clone().unwrap(); + project_dirs.cache_dir().with_file_name("cookies.json") } + +pub trait ExternMethod +where + Self: Sized, +{ + fn apply(self, method: F) -> Self + where + F: Fn(Self) -> Self, + { + method(self) + } + fn apply_with(self, method: F, options: O) -> Self + where + F: Fn(Self, O) -> Self, + { + method(self, options) + } +} + +impl ExternMethod for T {} diff --git a/core/src/users.rs b/core/src/users.rs new file mode 100644 index 0000000..fbf6bcf --- /dev/null +++ b/core/src/users.rs @@ -0,0 +1,60 @@ +pub mod permissions; +pub mod roles; + +use anyhow::anyhow; +use async_trait::async_trait; +use axum_session_auth::{Authentication, HasPermission}; +use chrono::{DateTime, Utc}; +use hashbrown::HashSet; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +use crate::database::get_user; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + pub id: i64, + pub anonymous: bool, + pub username: String, + pub created_at: Option>, + pub permissions: HashSet, +} + +impl Default for User { + fn default() -> Self { + Self { + id: 1, + anonymous: true, + username: "Guest".into(), + created_at: None, + permissions: HashSet::new(), + } + } +} + +#[async_trait] +impl Authentication for User { + async fn load_user(userid: i64, pool: Option<&PgPool>) -> Result { + let pool = pool.ok_or_else(|| anyhow!("expected a PgPool"))?; + Ok(get_user(pool, userid).await?) + } + + fn is_authenticated(&self) -> bool { + !self.anonymous + } + + fn is_active(&self) -> bool { + !self.anonymous + } + + fn is_anonymous(&self) -> bool { + self.anonymous + } +} + +#[async_trait] +impl HasPermission for User { + async fn has(&self, perm: &str, _pool: &Option<&PgPool>) -> bool { + self.permissions.contains(perm) + } +} diff --git a/core/src/users/permissions.rs b/core/src/users/permissions.rs new file mode 100644 index 0000000..b3551e3 --- /dev/null +++ b/core/src/users/permissions.rs @@ -0,0 +1,14 @@ +use sqlx::FromRow; + +#[derive(FromRow, Default, Debug, Clone)] +pub struct UserPermission { + pub token: String, +} + +impl UserPermission { + pub fn new(token: &str) -> Self { + Self { + token: token.to_owned(), + } + } +} diff --git a/core/src/users/roles.rs b/core/src/users/roles.rs new file mode 100644 index 0000000..2f86528 --- /dev/null +++ b/core/src/users/roles.rs @@ -0,0 +1,26 @@ +use crate::users::permissions::UserPermission; + +#[derive(Default)] +pub enum UserRole { + #[default] + Guest, + Normal, + Admin, +} + +impl UserRole { + pub fn permissions(&self) -> Vec { + match self { + UserRole::Guest => Vec::new(), + UserRole::Normal => vec![ + UserPermission::new("Users::View"), + UserPermission::new("CurrentUser::Manage"), + ], + UserRole::Admin => vec![ + UserPermission::new("Server::Manage"), + UserPermission::new("Users::View"), + UserPermission::new("Users::Manage"), + ], + } + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index c5f1ad2..4c51d9b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,11 +11,20 @@ ssr = ["server_fn/ssr", "server_fn_macro_default/ssr"] anyhow.workspace = true argon2.workspace = true axum.workspace = true +axum_session.workspace = true +axum_session_auth.workspace = true +axum_session_sqlx.workspace = true +bytes.workspace = true cfg-if.workspace = true +cookie_store.workspace = true dotenvy.workspace = true +futures.workspace = true +reqwest.workspace = true +reqwest_cookie_store.workspace = true server_fn.workspace = true sqlx.workspace = true tokio.workspace = true +tokio-tungstenite.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/server/migrations/20251202033116_create_database.sql b/server/migrations/20251202033116_create_database.sql index 2145ad6..c49ff32 100644 --- a/server/migrations/20251202033116_create_database.sql +++ b/server/migrations/20251202033116_create_database.sql @@ -1,6 +1,17 @@ CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, + anonymous BOOLEAN NOT NULL DEFAULT true, + username TEXT NOT NULL UNIQUE CHECK (username <> ''), + password TEXT NOT NULL CHECK (password <> ''), created_at TIMESTAMPTZ DEFAULT NOW() ); + +CREATE TABLE IF NOT EXISTS user_permissions ( + user_id BIGSERIAL NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token TEXT NOT NULL +); + +INSERT INTO users (id, username, password, anonymous) + VALUES (1, 'Guest', 'guest', true) + ON CONFLICT (id) + DO NOTHING; diff --git a/server/src/api.rs b/server/src/api.rs new file mode 100644 index 0000000..708dc24 --- /dev/null +++ b/server/src/api.rs @@ -0,0 +1,167 @@ +use kreqo_core::errors::ServerError; +use kreqo_core::users::User; +use server_fn_macro_default::server; + +use crate::custom_client::client::CustomClient; + +cfg_if::cfg_if! { + if #[cfg(feature = "ssr")] { + use argon2::{Argon2, PasswordHash, PasswordVerifier}; + use axum_session_auth::{Auth, Rights}; + use kreqo_core::database; + use kreqo_core::database::get_sql_user_from_username; + use kreqo_core::users::roles::UserRole; + use reqwest::Method; + use sqlx::PgPool; + + use crate::context::{auth, pool, context}; + + async fn require_perms(user: User, rights: Rights) -> Result<(), ServerError> { + if !Auth::::build([Method::POST], true) + .requires(rights) + .validate(&user, &Method::POST, None) + .await + { + return Err(ServerError::Unauthorized); + } + Ok(()) + } + } +} + +#[server] +pub async fn cleanup_expired_sessions() -> Result, ServerError> { + let auth = auth()?; + let current_user = auth.current_user.unwrap_or_default(); + require_perms(current_user, Rights::permission("Server::Manage")).await?; + + let removed_session_ids = auth.session.get_store().cleanup().await?; + Ok(removed_session_ids) +} + +#[server] +pub async fn current_user() -> Result { + let auth = auth()?; + Ok(auth.current_user.unwrap_or_default()) +} + +#[server] +pub async fn login(username: String, password: String) -> Result<(), ServerError> { + let (pool, auth) = context(); + + let sql_user = get_sql_user_from_username(pool, username) + .await + .map_err(|_| ServerError::WrongLogin)?; + + let parsed_hash = PasswordHash::new(&sql_user.password)?; + Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .map_err(|_| ServerError::WrongLogin)?; + + auth?.login_user(sql_user.id); + + Ok(()) +} + +#[server] +pub async fn logout() -> Result<(), ServerError> { + let auth = auth(); + auth?.logout_user(); + Ok(()) +} + +#[server] +pub async fn signup(username: String, password: String) -> Result { + let pool = pool(); + + #[cfg(debug_assertions)] + std::thread::sleep(std::time::Duration::from_millis(500)); + database::create_user(pool, username, password, UserRole::Normal).await +} + +#[server] +pub async fn get_users() -> Result, ServerError> { + let (pool, auth) = context(); + let current_user = auth?.current_user.unwrap_or_default(); + require_perms(current_user, Rights::permission("Users::View")).await?; + + #[cfg(debug_assertions)] + std::thread::sleep(std::time::Duration::from_millis(500)); + database::get_users(pool).await +} + +#[server] +pub async fn get_user(id: i64) -> Result { + let (pool, auth) = context(); + let current_user = auth?.current_user.unwrap_or_default(); + require_perms(current_user, Rights::permission("Users::View")).await?; + + #[cfg(debug_assertions)] + std::thread::sleep(std::time::Duration::from_millis(500)); + database::get_user(pool, id).await +} + +#[server] +pub async fn get_user_from_username(username: String) -> Result { + let (pool, auth) = context(); + let current_user = auth?.current_user.unwrap_or_default(); + require_perms(current_user, Rights::permission("Users::View")).await?; + + #[cfg(debug_assertions)] + std::thread::sleep(std::time::Duration::from_millis(500)); + database::get_user_from_username(pool, username).await +} + +#[server] +pub async fn update_user_username(id: i64, username: String) -> Result { + let (pool, auth) = context(); + let current_user = auth?.current_user.unwrap_or_default(); + + if id == 1 { + return Err(ServerError::Unauthorized); + } + + if id == current_user.id { + require_perms( + current_user, + Rights::any([ + Rights::permission("Users::Manage"), + Rights::permission("CurrentUser::Manage"), + ]), + ) + .await?; + } else { + require_perms(current_user, Rights::permission("Users::Manage")).await?; + } + + #[cfg(debug_assertions)] + std::thread::sleep(std::time::Duration::from_millis(500)); + database::update_user_username(pool, id, username).await +} + +#[server] +pub async fn delete_user(id: i64) -> Result { + let (pool, auth) = context(); + let current_user = auth?.current_user.unwrap_or_default(); + + if id == 1 { + return Err(ServerError::Unauthorized); + } + + if id == current_user.id { + require_perms( + current_user, + Rights::any([ + Rights::permission("Users::Manage"), + Rights::permission("CurrentUser::Manage"), + ]), + ) + .await?; + } else { + require_perms(current_user, Rights::permission("Users::Manage")).await?; + } + + #[cfg(debug_assertions)] + std::thread::sleep(std::time::Duration::from_millis(500)); + database::delete_user(pool, id).await +} diff --git a/server/src/context.rs b/server/src/context.rs new file mode 100644 index 0000000..637bfe3 --- /dev/null +++ b/server/src/context.rs @@ -0,0 +1,50 @@ +use std::env; +use std::sync::LazyLock; +use std::time::Duration; + +use axum::extract::Request; +use axum::middleware::Next; +use axum::response::Response; +use kreqo_core::errors::ServerError; +use sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; +use tokio::task_local; + +use crate::KreqoAuth; + +static POOL_CONTEXT: LazyLock = LazyLock::new(|| { + let db_connection_str = env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres@localhost/kreqo".to_string()); + + PgPoolOptions::new() + .max_connections(20) + .acquire_timeout(Duration::from_secs(3)) + .connect_lazy(&db_connection_str) + .expect("can't connect to database") +}); + +task_local! { + static AUTH_CONTEXT: KreqoAuth; +} + +pub async fn auth_context_middleware(auth: KreqoAuth, request: Request, next: Next) -> Response { + AUTH_CONTEXT.scope(auth, next.run(request)).await +} + +#[inline] +pub fn pool() -> &'static PgPool { + &POOL_CONTEXT +} + +#[inline] +pub fn auth() -> Result { + let auth = AUTH_CONTEXT + .try_with(|auth| auth.clone()) + .map_err(|error| ServerError::Session(error.to_string()))?; + Ok(auth) +} + +#[inline] +pub fn context() -> (&'static PgPool, Result) { + (pool(), auth()) +} diff --git a/server/src/custom_client.rs b/server/src/custom_client.rs new file mode 100644 index 0000000..72e3328 --- /dev/null +++ b/server/src/custom_client.rs @@ -0,0 +1,3 @@ +pub mod client; +pub mod request; +pub mod response; diff --git a/server/src/custom_client/client.rs b/server/src/custom_client/client.rs new file mode 100644 index 0000000..91b7eb8 --- /dev/null +++ b/server/src/custom_client/client.rs @@ -0,0 +1,74 @@ +use std::future::Future; + +use bytes::Bytes; +use futures::{FutureExt, SinkExt, StreamExt, TryFutureExt}; +use server_fn::client::{Client, get_server_url}; +use server_fn::error::{FromServerFnError, IntoAppError, ServerFnErrorErr}; + +use crate::custom_client::request::{CLIENT, CustomRequest}; +use crate::custom_client::response::CustomResponse; + +/// Implements [`Client`] for a request made by [`reqwest`]. +pub struct CustomClient; + +impl< + Error: FromServerFnError, + InputStreamError: FromServerFnError, + OutputStreamError: FromServerFnError, +> Client for CustomClient +{ + type Request = CustomRequest; + type Response = CustomResponse; + + fn send(req: Self::Request) -> impl Future> + Send { + CLIENT + .execute(req.0) + .map(|x| x.map(|res| res.into())) + .map_err(|e| ServerFnErrorErr::Request(e.to_string()).into_app_error()) + } + + async fn open_websocket( + path: &str, + ) -> Result< + ( + impl futures::Stream> + Send + 'static, + impl futures::Sink + Send + 'static, + ), + Error, + > { + let mut websocket_server_url = get_server_url().to_string(); + if let Some(postfix) = websocket_server_url.strip_prefix("http://") { + websocket_server_url = format!("ws://{postfix}"); + } else if let Some(postfix) = websocket_server_url.strip_prefix("https://") { + websocket_server_url = format!("wss://{postfix}"); + } + let url = format!("{websocket_server_url}{path}"); + let (ws_stream, _) = tokio_tungstenite::connect_async(url) + .await + .map_err(|e| Error::from_server_fn_error(ServerFnErrorErr::Request(e.to_string())))?; + + let (write, read) = ws_stream.split(); + + Ok( + ( + read.map(|msg| match msg { + Ok(msg) => Ok(msg.into_data()), + Err(e) => Err(OutputStreamError::from_server_fn_error( + ServerFnErrorErr::Request(e.to_string()), + ) + .ser()), + }), + write.with(|msg: Bytes| async move { + Ok::< + tokio_tungstenite::tungstenite::Message, + tokio_tungstenite::tungstenite::Error, + >(tokio_tungstenite::tungstenite::Message::Binary(msg)) + }), + ), + ) + } + + fn spawn(future: impl Future + Send + 'static) { + tokio::spawn(future); + } +} diff --git a/server/src/custom_client/request.rs b/server/src/custom_client/request.rs new file mode 100644 index 0000000..38ddfaf --- /dev/null +++ b/server/src/custom_client/request.rs @@ -0,0 +1,222 @@ +use std::fs::{self, OpenOptions}; +use std::io::BufReader; +use std::sync::{Arc, LazyLock}; + +use bytes::Bytes; +use cookie_store::CookieStore; +use futures::{Stream, StreamExt}; +use kreqo_core::cookies_path; +use reqwest::Body; +use reqwest::header::{ACCEPT, CONTENT_TYPE}; +use reqwest::multipart::Form; +pub use reqwest::{Client, Method, Request, Url}; +use reqwest_cookie_store::CookieStoreMutex; +use server_fn::client::get_server_url; +use server_fn::error::{FromServerFnError, IntoAppError, ServerFnErrorErr}; +use server_fn::request::ClientReq; + +pub(crate) static COOKIE_STORE: LazyLock> = LazyLock::new(|| { + let cookie_store = { + let path = cookies_path(); + if let Ok(file) = fs::File::open(path).map(BufReader::new) { + cookie_store::serde::json::load(file).unwrap_or_default() + } else { + CookieStore::new() + } + }; + Arc::new(CookieStoreMutex::new(cookie_store)) +}); + +pub(crate) static CLIENT: LazyLock = LazyLock::new(|| { + Client::builder() + .cookie_provider(Arc::clone(&COOKIE_STORE)) + .build() + .unwrap() +}); + +pub fn save_cookies() -> anyhow::Result<()> { + if let Ok(cookie_store) = COOKIE_STORE.lock() { + let path = cookies_path(); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; + cookie_store::serde::json::save(&cookie_store, &mut file).unwrap(); + } + Ok(()) +} + +pub struct CustomRequest(pub Request); + +impl From for CustomRequest { + fn from(value: Request) -> Self { + Self(value) + } +} + +impl ClientReq for CustomRequest +where + E: FromServerFnError, +{ + type FormData = Form; + + fn try_new_req_query( + path: &str, + content_type: &str, + accepts: &str, + query: &str, + method: Method, + ) -> Result { + let url = format!("{}{}", get_server_url(), path); + let mut url = Url::try_from(url.as_str()) + .map_err(|e| E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string())))?; + url.set_query(Some(query)); + let req = match method { + Method::GET => CLIENT.get(url), + Method::DELETE => CLIENT.delete(url), + Method::HEAD => CLIENT.head(url), + Method::POST => CLIENT.post(url), + Method::PATCH => CLIENT.patch(url), + Method::PUT => CLIENT.put(url), + m => { + return Err(E::from_server_fn_error( + ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()), + )); + } + } + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .build() + .map_err(|e| E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string())))?; + Ok(req.into()) + } + + fn try_new_req_text( + path: &str, + content_type: &str, + accepts: &str, + body: String, + method: Method, + ) -> Result { + let url = format!("{}{}", get_server_url(), path); + let req = match method { + Method::POST => CLIENT.post(url), + Method::PUT => CLIENT.put(url), + Method::PATCH => CLIENT.patch(url), + m => { + return Err(E::from_server_fn_error( + ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()), + )); + } + } + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .body(body) + .build() + .map_err(|e| ServerFnErrorErr::Request(e.to_string()).into_app_error())?; + Ok(req.into()) + } + + fn try_new_req_bytes( + path: &str, + content_type: &str, + accepts: &str, + body: Bytes, + method: Method, + ) -> Result { + let url = format!("{}{}", get_server_url(), path); + let req = match method { + Method::POST => CLIENT.post(url), + Method::PATCH => CLIENT.patch(url), + Method::PUT => CLIENT.put(url), + m => { + return Err(E::from_server_fn_error( + ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()), + )); + } + } + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .body(body) + .build() + .map_err(|e| ServerFnErrorErr::Request(e.to_string()).into_app_error())?; + Ok(req.into()) + } + + fn try_new_req_multipart( + path: &str, + accepts: &str, + body: Self::FormData, + method: Method, + ) -> Result { + let req = match method { + Method::POST => CLIENT.post(path), + Method::PUT => CLIENT.put(path), + Method::PATCH => CLIENT.patch(path), + m => { + return Err(E::from_server_fn_error( + ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()), + )); + } + } + .header(ACCEPT, accepts) + .multipart(body) + .build() + .map_err(|e| ServerFnErrorErr::Request(e.to_string()).into_app_error())?; + Ok(req.into()) + } + + fn try_new_req_form_data( + path: &str, + accepts: &str, + content_type: &str, + body: Self::FormData, + method: Method, + ) -> Result { + let req = match method { + Method::POST => CLIENT.post(path), + Method::PATCH => CLIENT.patch(path), + Method::PUT => CLIENT.put(path), + m => { + return Err(E::from_server_fn_error( + ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()), + )); + } + } + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .multipart(body) + .build() + .map_err(|e| ServerFnErrorErr::Request(e.to_string()).into_app_error())?; + Ok(req.into()) + } + + fn try_new_req_streaming( + path: &str, + accepts: &str, + content_type: &str, + body: impl Stream + Send + 'static, + method: Method, + ) -> Result { + let url = format!("{}{}", get_server_url(), path); + let body = + Body::wrap_stream(body.map(|chunk| Ok(chunk) as Result)); + let req = match method { + Method::POST => CLIENT.post(url), + Method::PUT => CLIENT.put(url), + Method::PATCH => CLIENT.patch(url), + m => { + return Err(E::from_server_fn_error( + ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()), + )); + } + } + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .body(body) + .build() + .map_err(|e| ServerFnErrorErr::Request(e.to_string()).into_app_error())?; + Ok(req.into()) + } +} diff --git a/server/src/custom_client/response.rs b/server/src/custom_client/response.rs new file mode 100644 index 0000000..aadd231 --- /dev/null +++ b/server/src/custom_client/response.rs @@ -0,0 +1,58 @@ +use bytes::Bytes; +use futures::{Stream, TryStreamExt}; +use reqwest::Response; +use server_fn::error::{FromServerFnError, IntoAppError, ServerFnErrorErr}; +use server_fn::response::ClientRes; + +pub struct CustomResponse(pub Response); + +impl From for CustomResponse { + fn from(value: Response) -> Self { + Self(value) + } +} + +impl ClientRes for CustomResponse { + async fn try_into_string(self) -> Result { + self.0 + .text() + .await + .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()) + } + + async fn try_into_bytes(self) -> Result { + self.0 + .bytes() + .await + .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()) + } + + fn try_into_stream( + self, + ) -> Result> + Send + 'static, E> { + Ok(self + .0 + .bytes_stream() + .map_err(|e| E::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser())) + } + + fn status(&self) -> u16 { + self.0.status().as_u16() + } + + fn status_text(&self) -> String { + self.0.status().to_string() + } + + fn location(&self) -> String { + self.0 + .headers() + .get("Location") + .map(|value| String::from_utf8_lossy(value.as_bytes()).to_string()) + .unwrap_or_else(|| self.0.url().to_string()) + } + + fn has_redirect(&self) -> bool { + self.0.headers().get("Location").is_some() + } +} diff --git a/server/src/database.rs b/server/src/database.rs deleted file mode 100644 index 5bd720a..0000000 --- a/server/src/database.rs +++ /dev/null @@ -1,112 +0,0 @@ -cfg_if::cfg_if! { - if #[cfg(feature = "ssr")] { - use argon2::{ - Argon2, - password_hash::{PasswordHasher, SaltString, rand_core::OsRng}, - }; - use crate::DB; - } -} -use kreqo_core::User; -use kreqo_core::errors::ServerError; -use server_fn_macro_default::server; - -#[server] -pub async fn get_users() -> Result, ServerError> { - let pool = &*DB; - - #[cfg(debug_assertions)] - std::thread::sleep(std::time::Duration::from_millis(500)); - - Ok(sqlx::query_as!(User, "SELECT * FROM users") - .fetch_all(pool) - .await?) -} - -#[server] -pub async fn get_user(id: i64) -> Result { - let pool = &*DB; - - #[cfg(debug_assertions)] - std::thread::sleep(std::time::Duration::from_millis(500)); - - Ok( - sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id) - .fetch_one(pool) - .await?, - ) -} - -#[server] -pub async fn get_user_from_username(username: String) -> Result { - let pool = &*DB; - - #[cfg(debug_assertions)] - std::thread::sleep(std::time::Duration::from_millis(500)); - - Ok( - sqlx::query_as!(User, "SELECT * FROM users WHERE username = $1", username) - .fetch_one(pool) - .await?, - ) -} - -#[server] -pub async fn create_user(username: String, password: String) -> Result { - let pool = &*DB; - - #[cfg(debug_assertions)] - std::thread::sleep(std::time::Duration::from_millis(500)); - - let salt = SaltString::generate(&mut OsRng); - let password_hashed = Argon2::default() - .hash_password(password.as_bytes(), &salt)? - .to_string(); - - let id = sqlx::query_scalar!( - "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id", - username.clone(), - password_hashed - ) - .fetch_one(pool) - .await?; - - // To check if the creation of the user was successfull - let user = get_user(id).await?; - - Ok(user) -} - -#[server] -pub async fn update_user_username(id: i64, username: String) -> Result { - let pool = &*DB; - - #[cfg(debug_assertions)] - std::thread::sleep(std::time::Duration::from_millis(500)); - - let id = sqlx::query_scalar!( - "UPDATE users SET username = $2 WHERE id = $1 RETURNING id", - id, - username.clone(), - ) - .fetch_one(pool) - .await?; - - let user = get_user(id).await?; - - Ok(user) -} - -#[server] -pub async fn delete_user(id: i64) -> Result { - let pool = &*DB; - - #[cfg(debug_assertions)] - std::thread::sleep(std::time::Duration::from_millis(500)); - - Ok( - sqlx::query_scalar!("DELETE FROM users WHERE id = $1 RETURNING id", id) - .fetch_one(pool) - .await?, - ) -} diff --git a/server/src/lib.rs b/server/src/lib.rs index d93814f..6b65b3d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,23 +1,13 @@ -pub mod database; +use axum_session_auth::AuthSession; +use axum_session_sqlx::SessionPgPool; +use kreqo_core::users::User; +use sqlx::PgPool; -cfg_if::cfg_if! { - if #[cfg(feature = "ssr")] { - use std::env; - use std::sync::LazyLock; - use std::time::Duration; +pub mod api; +#[cfg(feature = "ssr")] +pub mod context; +pub mod custom_client; - use sqlx::PgPool; - use sqlx::postgres::PgPoolOptions; +pub const SERVER_ADDRESS: &str = "localhost:8080"; - pub static DB: LazyLock = LazyLock::new(|| { - let db_connection_str = env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres@localhost/kreqo".to_string()); - - PgPoolOptions::new() - .max_connections(20) - .acquire_timeout(Duration::from_secs(3)) - .connect_lazy(&db_connection_str) - .expect("can't connect to database") - }); - } -} +pub type KreqoAuth = AuthSession; diff --git a/server/src/main.rs b/server/src/main.rs index 60e79a1..53f2a56 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,9 +1,16 @@ use std::env; use axum::Router; +use axum::middleware::from_fn; use axum::routing::{get, post}; -use kreqo_server::DB; +use axum_session::{SameSite, SessionConfig, SessionLayer, SessionStore}; +use axum_session_auth::{AuthConfig, AuthSessionLayer}; +use axum_session_sqlx::SessionPgPool; +use kreqo_core::users::User; +use kreqo_server::SERVER_ADDRESS; +use kreqo_server::context::{auth_context_middleware, pool}; use server_fn::axum::handle_server_fn; +use sqlx::PgPool; use tokio::net::TcpListener; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -20,17 +27,33 @@ async fn main() -> anyhow::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); + let pool = pool(); + sqlx::migrate!() - .run(&*DB) + .run(pool) .await .expect("database migrations failed"); + let session_config = SessionConfig::default() + .with_table_name("axum_sessions") + .with_secure(true) + .with_cookie_same_site(SameSite::Strict); + let auth_config = AuthConfig::::default().with_anonymous_user_id(Some(1)); + + let session_store = + SessionStore::::new(Some(pool.clone().into()), session_config).await?; + let router = Router::new() .route("/", get(|| async { "kreqo-server is running" })) - .route("/api/{*wildcard}", post(handle_server_fn)); - - let listener = TcpListener::bind("localhost:8080").await?; + .route("/api/{*wildcard}", post(handle_server_fn)) + .layer(from_fn(auth_context_middleware)) + .layer( + AuthSessionLayer::::new(Some(pool.clone())) + .with_config(auth_config), + ) + .layer(SessionLayer::new(session_store)); + let listener = TcpListener::bind(SERVER_ADDRESS).await?; tracing::debug!("listening on {}", listener.local_addr()?); axum::serve(listener, router).await?; diff --git a/server_fn_macro_default/src/lib.rs b/server_fn_macro_default/src/lib.rs index cbf13aa..edc6217 100644 --- a/server_fn_macro_default/src/lib.rs +++ b/server_fn_macro_default/src/lib.rs @@ -75,6 +75,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { s.into(), Some(syn::parse_quote!(server_fn)), option_env!("SERVER_FN_PREFIX").unwrap_or("/api"), + Some(syn::parse_quote! { CustomClient }), None, None, ) { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index cecee18..a102678 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -8,6 +8,8 @@ publish = false gh-workflow = "*" serde_json = "*" +kreqo-core.workspace = true + [[test]] name = "ci" path = "ci.rs" diff --git a/tests/ci.rs b/tests/ci.rs index 6aec1ba..99b933e 100644 --- a/tests/ci.rs +++ b/tests/ci.rs @@ -1,4 +1,5 @@ use gh_workflow::*; +use kreqo_core::ExternMethod; use serde_json::json; #[test] @@ -114,24 +115,3 @@ fn main() { workflow.generate().expect("workflow should generate"); } - -trait ExternMethod -where - Self: Sized, -{ - fn apply(self, method: F) -> Self - where - F: Fn(Self) -> Self, - { - method(self) - } - fn apply_with(self, method: F, options: O) -> Self - where - F: Fn(Self, O) -> Self, - { - method(self, options) - } -} - -impl ExternMethod for Job {} -impl ExternMethod for Step {} diff --git a/ui/Cargo.toml b/ui/Cargo.toml index c50b11f..12e8943 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -4,11 +4,13 @@ version = "0.0.0" edition.workspace = true [dependencies] +parley.workspace = true rapidfuzz.workspace = true server_fn.workspace = true thiserror.workspace = true uuid.workspace = true xilem.workspace = true +zxcvbn.workspace = true kreqo-core.workspace = true kreqo-server.workspace = true diff --git a/ui/src/auth_forms.rs b/ui/src/auth_forms.rs new file mode 100644 index 0000000..2a95198 --- /dev/null +++ b/ui/src/auth_forms.rs @@ -0,0 +1,339 @@ +use kreqo_core::users::User; +use thiserror::Error; +use xilem::masonry::layout::AsUnit; +use xilem::palette::css::GRAY; +use xilem::style::{Padding, Style}; +use xilem::tokio::sync::mpsc::UnboundedSender; +use xilem::view::{CrossAxisAlignment, flex_col, flex_row, inline_prose, text_input, zstack}; +use xilem::{Color, WidgetView}; +use zxcvbn::feedback::Feedback; +use zxcvbn::time_estimates::CrackTimes; +use zxcvbn::{Score, zxcvbn}; + +use crate::component::form::Submit; +use crate::component::{Form, action_button, form_input_label, header}; +use crate::theme::{ + ACCENT_COLOR, ApplyClass, CONTAINER, DANGER_COLOR, FORM_INPUT, SUCCESS_COLOR, WARNING_COLOR, + form_border_color, +}; + +#[derive(Debug, Error)] +pub enum UserError { + #[error("username is required")] + EmptyUsername, + #[error("password is required")] + EmptyPassword, + #[error("password confirmation doesn't match")] + PasswordConfirmationMismatch, + #[error("password is too weak")] + WeakPassword, +} + +impl UserError { + pub fn username_color(&self) -> Option { + matches!(self, UserError::EmptyUsername).then_some(DANGER_COLOR) + } + + pub fn password_color(&self) -> Option { + matches!(self, UserError::EmptyPassword).then_some(DANGER_COLOR) + } + + pub fn confirmation_color(&self) -> Option { + matches!(self, UserError::PasswordConfirmationMismatch).then_some(DANGER_COLOR) + } +} + +pub enum AuthRequest { + CleanupSessions, + Login(String, String), + Logout, + RefreshUser, +} + +#[derive(Debug)] +pub enum AuthMessage { + SessionsCleanedUp, + UserRefreshed(Option), +} + +#[derive(Debug, Default)] +pub struct UserLoginForm { + username: String, + password: String, + last_error: Option, +} + +impl Form for UserLoginForm { + type Output = (String, String); + type Error = UserError; + + fn last_error(&mut self) -> &mut Option { + &mut self.last_error + } + + // TODO: refactor into form fields + fn view(&mut self) -> impl WidgetView + use<> { + let header = header("Log into your account"); + let username = zstack(( + text_input(self.username.clone(), |state: &mut Self, input| { + state.username = input; + state.last_error = state.check().err(); + Submit::No + }) + .placeholder("username") + .text_color(ACCENT_COLOR) + .class(FORM_INPUT) + .apply( + form_border_color, + self.last_error.as_ref().and_then(UserError::username_color), + ), + form_input_label("Username"), + )); + let password = zstack(( + text_input(self.password.clone(), |state: &mut Self, input| { + state.password = input; + state.last_error = state.check().err(); + Submit::No + }) + .on_enter(|_, _| Submit::Yes) + .placeholder("password") + .text_color(ACCENT_COLOR) + .class(FORM_INPUT) + .apply( + form_border_color, + self.last_error.as_ref().and_then(UserError::password_color), + ), + form_input_label("Password"), + )); + let login_button = action_button("Log In", |_| Submit::Yes); + let error = self.error_view(); + flex_col((header, username, password, login_button, error)) + .class(CONTAINER) + .gap(30.px()) + } + + fn check(&mut self) -> Result<(), UserError> { + if self.username.is_empty() { + return Err(UserError::EmptyUsername); + } + if self.password.is_empty() { + return Err(UserError::EmptyPassword); + } + Ok(()) + } + + fn validate(&mut self) -> Result<(String, String), UserError> { + self.check()?; + Ok(( + std::mem::take(&mut self.username), + std::mem::take(&mut self.password), + )) + } +} + +impl UserLoginForm { + pub fn handle_submit(&mut self, submit: Submit, sender: Option<&UnboundedSender>) { + match submit { + Submit::No => (), + Submit::Cancel => { + self.reset(); + } + Submit::Yes => { + let output = self.submit(); + if let (Some((username, password)), Some(sender)) = (output, sender) { + let _ = sender.send(AuthRequest::Login(username, password)); + } + } + } + } +} + +#[derive(Debug)] +pub struct UserSignupForm { + username: String, + password: String, + password_confirmation: String, + score: Score, + feedback: Option, + crack_time: CrackTimes, + last_error: Option, +} + +impl Default for UserSignupForm { + fn default() -> Self { + Self { + username: String::new(), + password: String::new(), + password_confirmation: String::new(), + score: Score::Zero, + feedback: None, + crack_time: CrackTimes::new(0), + last_error: None, + } + } +} + +impl Form for UserSignupForm { + type Output = (String, String); + type Error = UserError; + + fn last_error(&mut self) -> &mut Option { + &mut self.last_error + } + + fn view(&mut self) -> impl WidgetView + use<> { + let header = header("Create your account"); + let username = zstack(( + text_input(self.username.clone(), |state: &mut Self, input| { + state.username = input; + state.last_error = state.check().err(); + Submit::No + }) + .placeholder("username") + .text_color(ACCENT_COLOR) + .class(FORM_INPUT) + .apply( + form_border_color, + self.last_error.as_ref().and_then(UserError::username_color), + ), + form_input_label("Username"), + )); + let password = flex_col(( + zstack(( + text_input(self.password.clone(), |state: &mut Self, input| { + state.password = input; + state.last_error = state.check().err(); + Submit::No + }) + .placeholder("password") + .text_color(ACCENT_COLOR) + .class(FORM_INPUT) + .apply( + form_border_color, + self.last_error.as_ref().and_then(UserError::password_color), + ), + form_input_label("Password"), + )), + (!self.password.is_empty()).then(|| { + let (color, text) = match self.score { + Score::Zero | Score::One => (DANGER_COLOR, "Very weak"), + Score::Two => (DANGER_COLOR, "Weak"), + Score::Three => (WARNING_COLOR, "Medium"), + _ => (SUCCESS_COLOR, "Strong"), + }; + let password_strength = flex_row(( + inline_prose("Password strength:").text_color(GRAY), + inline_prose(text).text_color(color), + )) + .padding(3.); + let crack_time = (self.score >= Score::Three).then_some( + flex_row(( + inline_prose(if self.score >= Score::Four { + " ✓ Time to crack:" + } else { + " ❌ Time to crack:" + }) + .text_size(13.) + .text_color(GRAY), + inline_prose( + self.crack_time + .offline_slow_hashing_1e4_per_second() + .to_string(), + ) + .text_size(13.) + .text_color(color), + )) + .padding(3.), + ); + let warning = self.feedback.as_ref().map(|feedback| { + feedback.warning().map(|warning| { + inline_prose(format!(" ❌ {}", warning)) + .text_size(13.) + .text_color(GRAY) + .padding(3.) + }) + }); + let suggestions = self.feedback.as_ref().map(|feedback| { + feedback + .suggestions() + .iter() + .map(|suggestion| { + inline_prose(format!(" ✓ {}", suggestion)) + .text_size(13.) + .text_color(GRAY) + .padding(3.) + }) + .collect::>() + }); + flex_col((password_strength, crack_time, warning, suggestions)) + .cross_axis_alignment(CrossAxisAlignment::Start) + .gap(0.px()) + .padding(Padding::horizontal(19.)) + }), + )); + let password_confirmation = zstack(( + text_input( + self.password_confirmation.clone(), + |state: &mut Self, input| { + state.password_confirmation = input; + state.last_error = state.check().err(); + Submit::No + }, + ) + .on_enter(|_, _| Submit::Yes) + .placeholder("confirm password") + .text_color(ACCENT_COLOR) + .class(FORM_INPUT) + .apply( + form_border_color, + self.last_error + .as_ref() + .and_then(UserError::confirmation_color), + ), + form_input_label("Password Confirmation"), + )); + let signup_button = action_button("Sign Up", |_| Submit::Yes); + let error = self.error_view(); + flex_col(( + header, + username, + password, + password_confirmation, + signup_button, + error, + )) + .class(CONTAINER) + .gap(30.px()) + } + + fn check(&mut self) -> Result<(), UserError> { + let entropy = zxcvbn(self.password.as_str(), &[self.username.as_str()]); + self.score = entropy.score(); + self.feedback = entropy.feedback().cloned(); + self.crack_time = entropy.crack_times(); + + if self.username.is_empty() { + return Err(UserError::EmptyUsername); + } + if self.password.is_empty() { + return Err(UserError::EmptyPassword); + } + if self.score < Score::Four { + return Err(UserError::WeakPassword); + } + if self.password != self.password_confirmation { + return Err(UserError::PasswordConfirmationMismatch); + } + + Ok(()) + } + + fn validate(&mut self) -> Result<(String, String), UserError> { + self.check()?; + self.password_confirmation = String::default(); + Ok(( + std::mem::take(&mut self.username), + std::mem::take(&mut self.password), + )) + } +} diff --git a/ui/src/class.rs b/ui/src/class.rs new file mode 100644 index 0000000..75b7a46 --- /dev/null +++ b/ui/src/class.rs @@ -0,0 +1,338 @@ +use xilem::WidgetView; +use xilem::masonry::core::{HasProperty, Property}; +use xilem::style::Style; +use xilem::view::Prop; + +pub trait Class { + type Styled; + fn styled(self, inner: Inner) -> Self::Styled; +} + +impl Class for (A, B) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + >::Widget: HasProperty + HasProperty, +{ + type Styled = Prop, State, Action>; + fn styled(self, inner: Inner) -> Self::Styled { + inner.prop(self.0).prop(self.1) + } +} + +impl Class for (A, B, C) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + C: Property + PartialEq, + >::Widget: HasProperty + HasProperty + HasProperty, +{ + type Styled = Prop, State, Action>, State, Action>; + fn styled(self, inner: Inner) -> Self::Styled { + inner.prop(self.0).prop(self.1).prop(self.2) + } +} + +impl Class for (A, B, C, D) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + C: Property + PartialEq, + D: Property + PartialEq, + >::Widget: + HasProperty + HasProperty + HasProperty + HasProperty, +{ + type Styled = Prop< + D, + Prop, State, Action>, State, Action>, + State, + Action, + >; + fn styled(self, inner: Inner) -> Self::Styled { + inner.prop(self.0).prop(self.1).prop(self.2).prop(self.3) + } +} + +impl Class for (A, B, C, D, E) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + C: Property + PartialEq, + D: Property + PartialEq, + E: Property + PartialEq, + >::Widget: + HasProperty + HasProperty + HasProperty + HasProperty + HasProperty, +{ + type Styled = Prop< + E, + Prop< + D, + Prop, State, Action>, State, Action>, + State, + Action, + >, + State, + Action, + >; + fn styled(self, inner: Inner) -> Self::Styled { + inner + .prop(self.0) + .prop(self.1) + .prop(self.2) + .prop(self.3) + .prop(self.4) + } +} + +impl Class for (A, B, C, D, E, F) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + C: Property + PartialEq, + D: Property + PartialEq, + E: Property + PartialEq, + F: Property + PartialEq, + >::Widget: HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty, +{ + type Styled = Prop< + F, + Prop< + E, + Prop< + D, + Prop, State, Action>, State, Action>, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >; + fn styled(self, inner: Inner) -> Self::Styled { + inner + .prop(self.0) + .prop(self.1) + .prop(self.2) + .prop(self.3) + .prop(self.4) + .prop(self.5) + } +} + +impl Class + for (A, B, C, D, E, F, G) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + C: Property + PartialEq, + D: Property + PartialEq, + E: Property + PartialEq, + F: Property + PartialEq, + G: Property + PartialEq, + >::Widget: HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty, +{ + type Styled = Prop< + G, + Prop< + F, + Prop< + E, + Prop< + D, + Prop, State, Action>, State, Action>, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >; + fn styled(self, inner: Inner) -> Self::Styled { + inner + .prop(self.0) + .prop(self.1) + .prop(self.2) + .prop(self.3) + .prop(self.4) + .prop(self.5) + .prop(self.6) + } +} + +impl Class + for (A, B, C, D, E, F, G, H) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + C: Property + PartialEq, + D: Property + PartialEq, + E: Property + PartialEq, + F: Property + PartialEq, + G: Property + PartialEq, + H: Property + PartialEq, + >::Widget: HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty, +{ + type Styled = Prop< + H, + Prop< + G, + Prop< + F, + Prop< + E, + Prop< + D, + Prop< + C, + Prop, State, Action>, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >; + fn styled(self, inner: Inner) -> Self::Styled { + inner + .prop(self.0) + .prop(self.1) + .prop(self.2) + .prop(self.3) + .prop(self.4) + .prop(self.5) + .prop(self.6) + .prop(self.7) + } +} + +impl Class + for (A, B, C, D, E, F, G, H, I) +where + State: 'static, + Action: 'static, + Inner: Style, + A: Property + PartialEq, + B: Property + PartialEq, + C: Property + PartialEq, + D: Property + PartialEq, + E: Property + PartialEq, + F: Property + PartialEq, + G: Property + PartialEq, + H: Property + PartialEq, + I: Property + PartialEq, + >::Widget: HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty + + HasProperty, +{ + type Styled = Prop< + I, + Prop< + H, + Prop< + G, + Prop< + F, + Prop< + E, + Prop< + D, + Prop< + C, + Prop, State, Action>, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >, + State, + Action, + >; + fn styled(self, inner: Inner) -> Self::Styled { + inner + .prop(self.0) + .prop(self.1) + .prop(self.2) + .prop(self.3) + .prop(self.4) + .prop(self.5) + .prop(self.6) + .prop(self.7) + .prop(self.8) + } +} diff --git a/ui/src/component.rs b/ui/src/component.rs index b10c642..14d7ffa 100644 --- a/ui/src/component.rs +++ b/ui/src/component.rs @@ -5,3 +5,108 @@ pub mod list; pub use error::ErrorView; pub use form::Form; pub use list::AsyncList; +use parley::LineHeight; +use parley::layout::{Alignment, AlignmentOptions}; +use xilem::core::frozen; +use xilem::masonry::core::{ArcStr, render_text}; +use xilem::masonry::layout::{AsUnit, Dim}; +use xilem::masonry::parley::{FontFamily, FontStack, GenericFamily, StyleProperty}; +use xilem::palette::css::WHITE; +use xilem::style::{Padding, Style}; +use xilem::vello::kurbo::{Affine, Circle, Point, Stroke}; +use xilem::vello::peniko::Fill; +use xilem::view::{ + CrossAxisAlignment, Prose, button, canvas, flex_col, flex_row, label, prose, sized_box, +}; +use xilem::{FontWeight, TextAlign, WidgetView}; + +use crate::theme::{ACCENT_COLOR, ACTION_BTN, ApplyClass, SURFACE_COLOR}; + +pub fn logo() -> impl WidgetView +where + State: 'static + Send + Sync, + Action: 'static + Send + Sync, +{ + frozen(|| { + flex_col(( + label("Kreqo") + .weight(FontWeight::BOLD) + .text_size(22.) + .transform(Affine::translate((-25., 7.))), + label("Learn") + .weight(FontWeight::EXTRA_BLACK) + .text_size(28.) + .color(ACCENT_COLOR) + .transform(Affine::translate((10., 0.))), + )) + .gap(0.px()) + }) +} + +pub fn user_profile_overview(username: &mut String) -> impl WidgetView + use<> { + let profile_circle = canvas(move |state: &mut String, ctx, scene, size| { + let (fcx, lcx) = ctx.text_contexts(); + let letter = &state[..1].to_uppercase(); + + let half_size = size.to_vec2() / 2.; + let circle = Circle::new(Point::new(half_size.x, half_size.y), half_size.x); + scene.fill(Fill::NonZero, Affine::IDENTITY, ACCENT_COLOR, None, &circle); + scene.stroke(&Stroke::default(), Affine::IDENTITY, WHITE, None, &circle); + + let mut text_layout_builder = lcx.ranged_builder(fcx, letter, 1., true); + text_layout_builder.push_default(StyleProperty::FontStack(FontStack::Single( + FontFamily::Generic(GenericFamily::SansSerif), + ))); + text_layout_builder.push_default(StyleProperty::FontSize(size.height as f32 * 0.75)); + text_layout_builder.push_default(StyleProperty::FontWeight(FontWeight::SEMI_BOLD)); + text_layout_builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute( + size.height as f32, + ))); + let mut text_layout = text_layout_builder.build(letter); + text_layout.break_all_lines(None); + text_layout.align( + Some(size.width as f32), + Alignment::Center, + AlignmentOptions::default(), + ); + render_text(scene, Affine::IDENTITY, &text_layout, &[WHITE.into()], true); + }); + flex_row(( + sized_box(profile_circle).dims(Dim::Fixed(35.px())), + prose(username.to_string()).text_size(18.), + )) +} + +pub fn header(content: impl Into) -> Prose { + prose(content) + .weight(FontWeight::BOLD) + .text_size(24.) + .text_alignment(TextAlign::Center) +} + +pub fn form_input_label(text: impl Into) -> impl WidgetView +where + State: 'static + Send + Sync, + Action: 'static + Send + Sync, +{ + flex_row( + label(text) + .text_size(13.) + .padding(3.) + .background(SURFACE_COLOR) + .transform(Affine::translate((0., -9.))), + ) + .cross_axis_alignment(CrossAxisAlignment::Start) + .padding(Padding::horizontal(19.)) +} + +pub fn action_button( + text: impl Into, + callback: impl Fn(&mut State) -> Action + Send + Sync + 'static, +) -> impl WidgetView +where + State: 'static, + Action: 'static, +{ + button(label(text).weight(FontWeight::BLACK), callback).class(ACTION_BTN) +} diff --git a/ui/src/component/error.rs b/ui/src/component/error.rs index 991c8fc..d877dbf 100644 --- a/ui/src/component/error.rs +++ b/ui/src/component/error.rs @@ -1,5 +1,4 @@ use xilem::WidgetView; -use xilem::core::Read; use xilem::style::Style; use xilem::view::{MainAxisAlignment, flex_row, prose}; @@ -9,14 +8,14 @@ pub trait ErrorView where Self: Sized + 'static, { - fn view(&self) -> impl WidgetView> + use; + fn view(&self) -> impl WidgetView + use; } impl ErrorView for T where T: ToString + 'static, { - fn view(&self) -> impl WidgetView> + use { + fn view(&self) -> impl WidgetView + use { flex_row(prose(self.to_string()).text_color(DANGER_COLOR)) .main_axis_alignment(MainAxisAlignment::Center) .padding(5.) diff --git a/ui/src/component/form.rs b/ui/src/component/form.rs index 321cc6e..389d192 100644 --- a/ui/src/component/form.rs +++ b/ui/src/component/form.rs @@ -1,5 +1,5 @@ use xilem::WidgetView; -use xilem::core::{Edit, map_action, map_state}; +use xilem::core::{map_action, map_state}; use xilem::view::{MainAxisAlignment, flex_row}; use crate::component::ErrorView; @@ -18,6 +18,9 @@ where type Error: ErrorView; fn last_error(&mut self) -> &mut Option; + fn check(&mut self) -> Result<(), Self::Error> { + Ok(()) + } /// This function should do three things: validate the form, reset it and then return the result. /// Ideally, the data returned in the output should be taken directly from memory with `std::mem::take`. If not possible, the method `Self::reset` can be used instead. fn validate(&mut self) -> Result; @@ -38,13 +41,13 @@ where } } - fn view(&mut self) -> impl WidgetView, Submit> + use; - fn error_view(&mut self) -> Option, Submit> + use> { + fn view(&mut self) -> impl WidgetView + use; + fn error_view(&mut self) -> Option + use> { self.last_error().as_ref().map(|error| { map_action( map_state( flex_row(error.view()).main_axis_alignment(MainAxisAlignment::Center), - move |state: &mut Self, ()| state.last_error().as_ref().unwrap(), + move |state: &mut Self| state.last_error().as_mut().unwrap(), ), |_, _| Submit::No, ) diff --git a/ui/src/component/list.rs b/ui/src/component/list.rs index 67d9082..8db393e 100644 --- a/ui/src/component/list.rs +++ b/ui/src/component/list.rs @@ -5,7 +5,7 @@ pub mod storage; use uuid::Uuid; use xilem::WidgetView; use xilem::core::one_of::Either; -use xilem::core::{Edit, MessageProxy, Read, fork, lens, map_action, map_state}; +use xilem::core::{MessageProxy, fork, lens, map_action, map_state}; use xilem::masonry::theme::BASIC_WIDGET_HEIGHT; use xilem::style::Style; use xilem::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -33,10 +33,10 @@ where fn view( &self, pending_item_operation: PendingItemOperation, - ) -> impl WidgetView, ItemAction> + use; + ) -> impl WidgetView> + use; fn pending_view( - create_output: &::Output, - ) -> impl WidgetView::Output>> + use { + create_output: &mut ::Output, + ) -> impl WidgetView<::Output> + use { let _ = create_output; spinner().height(BASIC_WIDGET_HEIGHT) } @@ -188,7 +188,7 @@ where T: ListItem, S: ListStorage, { - fn handle(self, state: &mut AsyncList) { + fn handle(self, state: &mut AsyncList) -> Option> { match self.data { ListMessage::FetchedAll(items) => state.items = items, ListMessage::Created(item) => { @@ -209,11 +209,11 @@ where state.resolve_pending_request(self.request_id); } *state.storage.last_error() = Some(error); - return; + return None; } } - state.resolve_pending_request(self.request_id); *state.storage.last_error() = None; + state.resolve_pending_request(self.request_id) } } @@ -280,19 +280,20 @@ where } } - fn resolve_pending_request(&mut self, request_id: Uuid) { + fn resolve_pending_request(&mut self, request_id: Uuid) -> Option> { if let Some(index) = self .pending_requests .iter() .enumerate() .find_map(|(i, pending)| (request_id == pending.request_id).then_some(i)) { - self.pending_requests.remove(index); + return Some(self.pending_requests.remove(index).data); } + None } - fn get(&self, id: T::Id) -> Option<&T> { - self.items.iter().find(|item| item.id() == id) + fn get(&mut self, id: T::Id) -> Option<&mut T> { + self.items.iter_mut().find(|item| item.id() == id) } fn get_mut(&mut self, id: T::Id) -> Option<&mut T> { @@ -345,13 +346,12 @@ where pending_item_operation: PendingItemOperation, id: T::Id, item: &T, - ) -> impl WidgetView> + use { + ) -> impl WidgetView + use { if editing { Either::A(map_action( - lens( - ::view, - move |state: &mut Self, ()| &mut state.update_form, - ), + lens(::view, move |state: &mut Self| { + &mut state.update_form + }), move |state: &mut Self, submit| { state.handle_update_submit(id, submit); }, @@ -360,7 +360,7 @@ where Either::B(map_action( map_state( item.view(pending_item_operation), - move |state: &mut Self, ()| state.get(id).unwrap(), + move |state: &mut Self| state.get(id).unwrap(), ), move |state: &mut Self, action| { action.handle(state, id); @@ -369,7 +369,7 @@ where } } - fn process_items(&mut self) -> impl Iterator> + use> { + fn process_items(&mut self) -> impl Iterator + use> { self.processed_items = self .items .iter() @@ -391,16 +391,19 @@ where }) } - fn process_pending_items( - &self, - ) -> impl Iterator> + use> { + fn process_pending_items(&mut self) -> impl Iterator + use> { self.pending_requests - .iter() + .iter_mut() .enumerate() .filter_map(|(i, pending_request)| { matches!(pending_request.data, ListRequest::Create(_)).then_some(lens( T::pending_view, - move |state: &mut Self, ()| match &state.pending_requests.get(i).unwrap().data { + move |state: &mut Self| match &mut state + .pending_requests + .get_mut(i) + .unwrap() + .data + { ListRequest::Create(create_output) => create_output, _ => unreachable!(), }, @@ -408,30 +411,52 @@ where }) } - pub fn view(&mut self) -> impl WidgetView> + use { - let create_line = map_action( - lens( - ::view, - move |state: &mut Self, ()| &mut state.create_form, - ), + pub fn create_view(&mut self) -> impl WidgetView + use { + map_action( + lens(::view, move |state: &mut Self| { + &mut state.create_form + }), |state: &mut Self, submit| { state.handle_create_submit(submit); }, - ); - let filter_line = self.filter.as_mut().map(|filter| { - map_state(filter.view(), move |state: &mut Self, ()| { + ) + } + + // TODO: refactor into list view layout options + pub fn view(&mut self) -> impl WidgetView + use { + let filter = self.filter.as_mut().map(|filter| { + map_state(filter.view(), move |state: &mut Self| { state.filter.as_mut().unwrap() }) }); - let sorter_line = self.sorter.as_mut().map(|sorter| { - map_state(sorter.view(), move |state: &mut Self, ()| { + let sorter = self.sorter.as_mut().map(|sorter| { + map_state(sorter.view(), move |state: &mut Self| { state.sorter.as_mut().unwrap() }) }); let items = self.process_items().collect::>(); let pending_items = self.process_pending_items().collect::>(); + flex_col((filter, sorter, items, pending_items)) + } + + // TODO: refactor into error display from context + pub fn error_view(&mut self) -> Option + use> { + self.storage.last_error().as_ref().map(|error| { + map_state(error.view(), move |state: &mut Self| { + state.storage.last_error().as_mut().unwrap() + }) + }) + } + + // TODO: refactor storage to be more general and ergonomic + pub fn worker( + child: Child, + ) -> impl WidgetView>> + use + where + Child: WidgetView, + { fork( - flex_col((create_line, filter_line, sorter_line, items, pending_items)), + map_action(child, |_, _| None), worker( |proxy, mut rx: UnboundedReceiver>>| async move { while let Some(pending_request) = rx.recv().await { @@ -443,17 +468,9 @@ where state.send_request(ListRequest::FetchAll); }, |state: &mut Self, pending_message: Pending>| { - pending_message.handle(state); + pending_message.handle(state) }, ), ) } - - pub fn error_view(&mut self) -> Option> + use> { - self.storage.last_error().as_ref().map(|error| { - map_state(error.view(), move |state: &mut Self, ()| { - state.storage.last_error().as_ref().unwrap() - }) - }) - } } diff --git a/ui/src/component/list/filter.rs b/ui/src/component/list/filter.rs index 3ead358..9dc852b 100644 --- a/ui/src/component/list/filter.rs +++ b/ui/src/component/list/filter.rs @@ -1,5 +1,4 @@ use xilem::WidgetView; -use xilem::core::Edit; use xilem::view::flex_row; use crate::component::list::ListItem; @@ -10,7 +9,7 @@ where { type Item; - fn view(&mut self) -> impl WidgetView> + use; + fn view(&mut self) -> impl WidgetView + use; /// This function should return `(filter, score)` where a true `filter` value means the item /// should be included and where `score` is the matching score used in sorting. `score` should /// be between `0.0` and `1.0`. To disable filtering completely, please always return @@ -27,7 +26,7 @@ where { type Item = T; - fn view(&mut self) -> impl WidgetView> + use { + fn view(&mut self) -> impl WidgetView + use { flex_row(()) } diff --git a/ui/src/component/list/sorter.rs b/ui/src/component/list/sorter.rs index dd85139..596f987 100644 --- a/ui/src/component/list/sorter.rs +++ b/ui/src/component/list/sorter.rs @@ -1,7 +1,6 @@ use std::cmp::Ordering; use xilem::WidgetView; -use xilem::core::Edit; use xilem::view::flex_row; use crate::component::list::ListItem; @@ -13,7 +12,7 @@ where type Item; fn enabled(&self) -> bool; - fn view(&mut self) -> impl WidgetView> + use; + fn view(&mut self) -> impl WidgetView + use; fn sort(&self, a: &Self::Item, b: &Self::Item, score_a: f32, score_b: f32) -> Ordering; } @@ -30,7 +29,7 @@ where false } - fn view(&mut self) -> impl WidgetView> + use { + fn view(&mut self) -> impl WidgetView + use { flex_row(()) } diff --git a/ui/src/lib.rs b/ui/src/lib.rs index b552330..6cda70b 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -1,3 +1,5 @@ +pub mod auth_forms; +pub mod class; pub mod component; pub mod pending; pub mod theme; diff --git a/ui/src/theme.rs b/ui/src/theme.rs index 7cc934d..9eb52a5 100644 --- a/ui/src/theme.rs +++ b/ui/src/theme.rs @@ -1,17 +1,122 @@ use xilem::Color; use xilem::masonry::core::DefaultProperties; +use xilem::masonry::layout::Dim; +use xilem::masonry::properties::{ + Dimensions, FocusedBorderColor, PlaceholderColor, SelectionColor, +}; +use xilem::masonry::theme::ZYNC_600; use xilem::masonry::widgets::TextInput; -use xilem::style::Background; +use xilem::palette::css::{BLACK, TRANSPARENT, WHITE}; +use xilem::style::{ + ActiveBackground, Background, BorderColor, BorderWidth, CornerRadius, HoveredBorderColor, + Padding, +}; + +use crate::class::Class; + +pub const DARK_OVERLAY: Color = BLACK.with_alpha(0.25); pub const BACKGROUND_COLOR: Color = Color::from_rgb8(0x0a, 0x0a, 0x0a); pub const SURFACE_COLOR: Color = Color::from_rgb8(0x14, 0x14, 0x14); pub const SURFACE_BORDER_COLOR: Color = Color::from_rgb8(0x1e, 0x1e, 0x1e); -pub const SUCCESS_COLOR: Color = Color::from_rgb8(0x37, 0xc8, 0x37); -pub const DANGER_COLOR: Color = Color::from_rgb8(0xc8, 0x37, 0x37); +pub const ACCENT_COLOR: Color = Color::from_rgb8(0x00, 0x92, 0xb8); +pub const ACTIVE_ACCENT_COLOR: Color = Color::from_rgb8(0x00, 0xb8, 0xdb); +pub const SELECTION_ACCENT_COLOR: Color = Color::from_rgb8(0xe2, 0xe8, 0xf0); + +pub const SUCCESS_COLOR: Color = Color::from_rgb8(0x00, 0xbc, 0x7d); +pub const WARNING_COLOR: Color = Color::from_rgb8(0xfd, 0x9a, 0x00); +pub const DANGER_COLOR: Color = Color::from_rgb8(0xfb, 0x2c, 0x36); pub fn apply_theme(def_props: &mut DefaultProperties) { def_props.insert::(Background::Color( SURFACE_COLOR.map_lightness(|l| l * 0.95), )); } + +pub const SURFACE: (Background, BorderWidth, BorderColor) = ( + Background::Color(SURFACE_COLOR), + BorderWidth::all(1.), + BorderColor::new(SURFACE_BORDER_COLOR), +); + +pub const CONTAINER: (Padding, CornerRadius, Background, BorderWidth, BorderColor) = ( + Padding::all(25.), + CornerRadius::all(15.), + SURFACE.0, + SURFACE.1, + SURFACE.2, +); + +pub const ROW: (Padding, CornerRadius, Background) = ( + Padding::all(5.), + CornerRadius::all(10.), + Background::Color(SURFACE_COLOR), +); + +pub const ROW_OVERLAY: (Padding, CornerRadius, Background) = + (ROW.0, ROW.1, Background::Color(DARK_OVERLAY)); + +pub const BORDERED_ROW: (Padding, CornerRadius, Background, BorderWidth, BorderColor) = + (ROW.0, ROW.1, SURFACE.0, SURFACE.1, SURFACE.2); + +pub const FORM_INPUT: (Padding, CornerRadius, PlaceholderColor, SelectionColor) = ( + Padding::from_vh(15., 25.), + CornerRadius::all(7.5), + PlaceholderColor::new(WHITE.with_alpha(0.25)), + SelectionColor { + color: SELECTION_ACCENT_COLOR, + }, +); + +pub const ACTION_BTN: ( + Dimensions, + Padding, + CornerRadius, + Background, + ActiveBackground, + BorderColor, + HoveredBorderColor, +) = ( + Dimensions::new(Dim::Stretch, Dim::Auto), + Padding::from_vh(10., 25.), + FORM_INPUT.1, + Background::Color(ACCENT_COLOR), + ActiveBackground(Background::Color(ACTIVE_ACCENT_COLOR)), + BorderColor::new(TRANSPARENT), + HoveredBorderColor(BorderColor::new(WHITE)), +); + +pub fn form_border_color(color: Option) -> (BorderColor, FocusedBorderColor) { + match color { + Some(color) => ( + BorderColor::new(color), + FocusedBorderColor(BorderColor::new(color)), + ), + None => ( + BorderColor::new(ZYNC_600), + FocusedBorderColor(BorderColor::new(ACCENT_COLOR)), + ), + } +} + +pub trait ApplyClass { + fn class(self, class: C) -> C::Styled + where + Self: Sized, + C: Class, + { + class.styled(self) + } + + fn apply(self, f: F, input: I) -> C::Styled + where + Self: Sized, + C: Class, + F: FnOnce(I) -> C, + { + f(input).styled(self) + } +} + +impl ApplyClass for T where C: Class {} diff --git a/ui/src/user_list.rs b/ui/src/user_list.rs index 122f279..f86a0be 100644 --- a/ui/src/user_list.rs +++ b/ui/src/user_list.rs @@ -1,15 +1,12 @@ use std::cmp::Ordering; -use kreqo_core::User; use kreqo_core::errors::ServerError; -use kreqo_server::database::{create_user, delete_user, get_users, update_user_username}; +use kreqo_core::users::User; +use kreqo_server::api::{delete_user, get_users, signup, update_user_username}; use rapidfuzz::distance::jaro; use server_fn::error::ServerFnErrorErr; -use thiserror::Error; use xilem::core::one_of::Either; -use xilem::core::{Edit, Read}; -use xilem::masonry::layout::{AsUnit, Dim}; -use xilem::palette::css::BLACK; +use xilem::masonry::layout::AsUnit; use xilem::style::Style; use xilem::view::{ FlexExt, MainAxisAlignment, button, flex_col, flex_row, label, prose, spinner, text_button, @@ -17,97 +14,16 @@ use xilem::view::{ }; use xilem::{TextAlign, WidgetView}; +use crate::auth_forms::{UserError, UserSignupForm}; use crate::component::Form; use crate::component::form::Submit; use crate::component::list::storage::Retryable; use crate::component::list::{ ItemAction, ListFilter, ListItem, ListSorter, ListStorage, PendingItemOperation, }; -use crate::theme::{DANGER_COLOR, SUCCESS_COLOR, SURFACE_BORDER_COLOR, SURFACE_COLOR}; - -#[derive(Debug, Error)] -pub enum UserError { - #[error("username is required")] - EmptyUsername, - #[error("password is required")] - EmptyPassword, - #[error("password confirmation doesn't match")] - PasswordConfirmationMismatch, -} - -#[derive(Debug, Default)] -pub struct CreateUserForm { - username: String, - password: String, - password_confirmation: String, - last_error: Option, -} - -impl Form for CreateUserForm { - type Output = (String, String); - type Error = UserError; - - fn last_error(&mut self) -> &mut Option { - &mut self.last_error - } - - fn view(&mut self) -> impl WidgetView, Submit> + use<> { - let username = text_input( - self.username.clone(), - |state: &mut CreateUserForm, input| { - state.username = input; - Submit::No - }, - ) - .placeholder("Username"); - let password = text_input( - self.password.clone(), - |state: &mut CreateUserForm, input| { - state.password = input; - Submit::No - }, - ) - .placeholder("Password"); - let password_confirmation = text_input( - self.password_confirmation.clone(), - |state: &mut CreateUserForm, input| { - state.password_confirmation = input; - Submit::No - }, - ) - .placeholder("Password confirmation"); - let signup_button = text_button("Signup", |_| Submit::Yes).width(Dim::Stretch); - let error = self.error_view(); - flex_col(( - username, - password, - password_confirmation, - signup_button, - error, - )) - .padding(25.) - .corner_radius(15.) - .background_color(SURFACE_COLOR) - .border(SURFACE_BORDER_COLOR, 1.) - } - - fn validate(&mut self) -> Result<(String, String), UserError> { - if self.username.is_empty() { - return Err(UserError::EmptyUsername); - } - if self.password.is_empty() { - return Err(UserError::EmptyPassword); - } - if self.password != self.password_confirmation { - return Err(UserError::PasswordConfirmationMismatch); - } - self.password_confirmation = String::default(); - Ok(( - std::mem::take(&mut self.username), - std::mem::take(&mut self.password), - )) - } -} +use crate::theme::{ + ApplyClass, BORDERED_ROW, DANGER_COLOR, ROW, ROW_OVERLAY, SUCCESS_COLOR, form_border_color, +}; #[derive(Debug, Default)] pub struct UpdateUserForm { @@ -123,16 +39,21 @@ impl Form for UpdateUserForm { &mut self.last_error } - fn view(&mut self) -> impl WidgetView, Submit> + use<> { + fn view(&mut self) -> impl WidgetView + use<> { let username = text_input( self.username.clone(), |state: &mut UpdateUserForm, input| { state.username = input; + state.last_error = state.check().err(); Submit::No }, ) .on_enter(|_, _| Submit::Yes) - .placeholder("Username"); + .placeholder("Username") + .apply( + form_border_color, + self.last_error.as_ref().and_then(UserError::username_color), + ); let ok_button = button(label("Ok").color(SUCCESS_COLOR), |_| Submit::Yes); let cancel_button = text_button("Cancel", |_| Submit::Cancel); let error = self.error_view(); @@ -140,16 +61,18 @@ impl Form for UpdateUserForm { flex_row((username.flex(1.), ok_button, cancel_button)), error, )) - .padding(5.) - .corner_radius(10.) - .background_color(SURFACE_COLOR) - .border(SURFACE_BORDER_COLOR, 1.) + .class(BORDERED_ROW) } - fn validate(&mut self) -> Result { + fn check(&mut self) -> Result<(), UserError> { if self.username.is_empty() { return Err(UserError::EmptyUsername); } + Ok(()) + } + + fn validate(&mut self) -> Result { + self.check()?; Ok(std::mem::take(&mut self.username)) } } @@ -189,7 +112,7 @@ impl ListStorage for UserStorage { #[inline(always)] async fn create((username, password): (String, String)) -> Result { - create_user(username, password).await + signup(username, password).await } #[inline(always)] @@ -211,7 +134,7 @@ pub struct UserFilter { impl ListFilter for UserFilter { type Item = User; - fn view(&mut self) -> impl WidgetView> + use<> { + fn view(&mut self) -> impl WidgetView + use<> { let username_search = text_input(self.by_username.clone(), |state: &mut Self, input| { state.by_username = input; }) @@ -299,7 +222,7 @@ impl ListSorter for UserSorter { self.enabled } - fn view(&mut self) -> impl WidgetView> + use<> { + fn view(&mut self) -> impl WidgetView + use<> { let sorter = if self.enabled { let sort_by = text_button(self.sort_by.to_string(), |state: &mut Self| { state.sort_by = state.sort_by.next(); @@ -338,7 +261,7 @@ impl ListSorter for UserSorter { impl ListItem for User { type Id = i64; - type CreateForm = CreateUserForm; + type CreateForm = UserSignupForm; type UpdateForm = UpdateUserForm; type Filter = UserFilter; type Sorter = UserSorter; @@ -350,7 +273,7 @@ impl ListItem for User { fn view( &self, pending_item_operation: PendingItemOperation, - ) -> impl WidgetView, ItemAction> + use<> { + ) -> impl WidgetView> + use<> { let id = prose(format!("{}", self.id)) .text_alignment(TextAlign::Center) .width(25.px()); @@ -368,29 +291,21 @@ impl ListItem for User { ItemAction::Delete })) }; - flex_row((id, username.flex(1.), edit_button, delete_button)) - .padding(5.) - .corner_radius(10.) - .background_color(SURFACE_COLOR) - .border(SURFACE_BORDER_COLOR, 1.) + flex_row((id, username.flex(1.), edit_button, delete_button)).class(BORDERED_ROW) } fn pending_view( - (username, _): &(String, String), - ) -> impl WidgetView> + use<> { + (username, _): &mut (String, String), + ) -> impl WidgetView<(String, String)> + use<> { let id = prose("⏳").text_alignment(TextAlign::Center).width(25.px()); let username = prose(username.to_string()); let edit_button = text_button("Edit", |_| {}).disabled(true); let delete_button = text_button("Delete", |_| {}).disabled(true); - let pending_layer = flex_row((id, username.flex(1.), edit_button, delete_button)) - .padding(5.) - .corner_radius(10.) - .background_color(SURFACE_COLOR); + let pending_layer = + flex_row((id, username.flex(1.), edit_button, delete_button)).class(ROW); let spinner_layer = flex_row(spinner()) .main_axis_alignment(MainAxisAlignment::Center) - .padding(5.) - .corner_radius(10.) - .background_color(BLACK.with_alpha(0.25)); + .class(ROW_OVERLAY); zstack((pending_layer, spinner_layer)) } }