From e4a47ab21dd10eb2b4895219f355129cb160d1ae Mon Sep 17 00:00:00 2001 From: James Kerr Date: Fri, 13 Jun 2025 13:24:23 +1200 Subject: [PATCH 01/19] feat: Add Steam user authentication and token issuing This change adds user authentication using Steam's OpenID 2.0 as a backend. This change also adds support for issuing Biscuit tokens for session information. --- Cargo.lock | 436 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 7 +- 2 files changed, 425 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcb84cf..94be77d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,6 +571,62 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "biscuit-auth" +version = "6.0.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336ffc076d302968607866acb85378ed77066289ce1868aa7770c0f3f6e83e5c" +dependencies = [ + "base64 0.13.1", + "biscuit-parser", + "biscuit-quote", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "getrandom 0.2.16", + "hex", + "nom", + "p256", + "pkcs8 0.9.0", + "prost", + "prost-types", + "rand 0.8.5", + "rand_core 0.6.4", + "regex", + "serde_json", + "sha2 0.9.9", + "thiserror 1.0.69", + "time", + "zeroize", +] + +[[package]] +name = "biscuit-parser" +version = "0.2.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6f2eb85dcc822200621ce077b9382453d28ddd6a55e7a97bdb592370d2d440" +dependencies = [ + "hex", + "nom", + "proc-macro2", + "quote", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "biscuit-quote" +version = "0.3.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c624b1cf44110708a62b63341b17c37c33fcdd3cce89b501a115af3454dcf8" +dependencies = [ + "biscuit-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -619,7 +675,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -635,6 +691,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1197,6 +1262,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-random" version = "0.1.18" @@ -1243,7 +1314,7 @@ dependencies = [ "hmac", "percent-encoding", "rand 0.8.5", - "sha2", + "sha2 0.10.9", "subtle", "time", "version_check", @@ -1324,6 +1395,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1388,6 +1471,33 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "darling" version = "0.20.11" @@ -1456,6 +1566,26 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1523,13 +1653,23 @@ dependencies = [ "nu-ansi-term 0.50.1", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -1659,12 +1799,73 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.3" @@ -1832,6 +2033,22 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.25" @@ -2302,6 +2519,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2404,6 +2622,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.10" @@ -2563,7 +2792,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -4144,7 +4373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -4607,7 +4836,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tar", "thiserror 1.0.69", "toml", @@ -4733,6 +4962,18 @@ dependencies = [ "supports-color 3.0.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -4803,10 +5044,10 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", + "digest 0.10.7", "hmac", "password-hash", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4819,6 +5060,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4867,7 +5117,7 @@ checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4982,6 +5232,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -5049,6 +5319,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -5102,6 +5381,39 @@ dependencies = [ "yansi", ] +[[package]] +name = "prost" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "psl-types" version = "2.0.11" @@ -5573,6 +5885,16 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -5728,7 +6050,7 @@ version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -6072,7 +6394,7 @@ dependencies = [ "rustls-pemfile", "salvo_macros", "serde", - "serde-xml-rs", + "serde-xml-rs 0.6.0", "serde_json", "sync_wrapper", "tempfile", @@ -6152,7 +6474,7 @@ dependencies = [ "password-hash", "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -6161,6 +6483,21 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -6259,6 +6596,18 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "serde-xml-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" +dependencies = [ + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -6367,6 +6716,16 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6375,7 +6734,20 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", ] [[package]] @@ -6386,7 +6758,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -6404,6 +6776,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -6524,6 +6906,25 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + [[package]] name = "spm_precompiled" version = "0.1.4" @@ -6803,7 +7204,7 @@ dependencies = [ "serde-content", "serde_json", "sha1", - "sha2", + "sha2 0.10.9", "snap", "storekey", "strsim", @@ -6847,7 +7248,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "surrealdb", "tokio", ] @@ -8340,6 +8741,8 @@ name = "workshop-walker" version = "0.1.0" dependencies = [ "bbscope", + "biscuit-auth", + "chrono", "config", "humantime", "indicatif", @@ -8348,6 +8751,7 @@ dependencies = [ "reqwest", "salvo", "serde", + "serde-xml-rs 0.8.1", "serde_json", "serde_repr", "snafu", diff --git a/Cargo.toml b/Cargo.toml index 4fee62f..a881586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,13 @@ license = "MPL-2" [dependencies] tokio = { version = "1.44", features = ["rt", "rt-multi-thread", "macros", "fs", "sync"] } surrealdb = { version = "2.3", features = ["kv-rocksdb", "allocator", "protocol-http", "protocol-ws", "rustls"] } -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.12", features = ["json", ] } indicatif = "0.17.11" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_repr = "0.1" snafu = "0.8" -salvo = { version = "0.78", features = ["oapi", "logging", "serve-static"] } +salvo = { version = "0.78", features = ["oapi", "logging", "serve-static", "affix-state", "cookie"] } config = "0.15" veil = "0.2" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } @@ -32,6 +32,9 @@ bbscope = "0.2" humantime = "2.2" tracing = "0.1" surrealdb-migrations = "2.2" +serde-xml-rs = "0.8.1" +chrono = "0.4" +biscuit-auth = "6.0.0-beta.3" [patch.crates-io] surrealdb-migrations = { git = "https://github.com/disconsented/surrealdb-migrations.git" } From 8c176bb9b70328e9dd248f9a3d6c014eba5a53e1 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Fri, 13 Jun 2025 13:25:25 +1200 Subject: [PATCH 02/19] feat: Add Steam user authentication and token issuing This change adds user authentication using Steam's OpenID 2.0 as a backend. This change also adds support for issuing Biscuit tokens for session information. --- src/app_config.rs | 46 +++++++++- src/auth.rs | 226 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 +- src/web/mod.rs | 16 +++- 4 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 src/auth.rs diff --git a/src/app_config.rs b/src/app_config.rs index a0323f6..54398c5 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -1,25 +1,61 @@ -use std::sync::Arc; +use std::{collections::HashMap, str::FromStr, sync::Arc}; -use serde::{Deserialize, Serialize}; +use biscuit_auth::PrivateKey; +use serde::{Deserialize, Deserializer}; use veil::Redact; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Deserialize, Debug)] pub struct Config { pub steam: Steam, pub database: Database, pub read_from_cache: bool, pub download_workshop: bool, pub updater: bool, + pub base_url: Arc, + pub biscuit: Arc, } -#[derive(Serialize, Deserialize, Redact)] +#[derive(Deserialize, Redact)] pub struct Steam { #[redact] pub api_token: Arc, pub appid: u32, } -#[derive(Serialize, Deserialize, Redact)] +#[derive(Deserialize, Redact)] pub struct Database { pub user: String, #[redact] pub password: String, } + +#[derive(Redact)] +#[redact(all)] +pub struct Biscuit { + pub private_key: PrivateKey, + // pub lifetime: Duration, +} + +impl<'de> serde::Deserialize<'de> for Biscuit { + fn deserialize>(d: D) -> Result { + let mut map: HashMap = HashMap::deserialize(d)?; + Ok(Self { + private_key: map + .remove("private_key") + .as_deref() + .map(FromStr::from_str) + .unwrap() + .unwrap(), + }) + } +} + +#[cfg(test)] +mod test { + use biscuit_auth::KeyPair; + + #[test] + fn test_keygen() { + let pair = KeyPair::new(); + println!("{}", pair.public().print()); + println!("{}", pair.private().to_prefixed_string()); + } +} diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..aa86fb4 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,226 @@ +use std::{str::FromStr, sync::Arc}; + +use biscuit_auth::{KeyPair, macros::biscuit}; +use chrono::{NaiveDateTime, TimeDelta, Utc}; +use reqwest::{Client, Url}; +use salvo::{ + Depot, Request, Response, handler, + http::cookie::{Cookie, SameSite, time::Duration}, + prelude::{Redirect, StatusCode, StatusError}, +}; +use serde::{Deserialize, Serialize}; +use serde_xml_rs::from_str; +use snafu::{ErrorCompat, prelude::*}; +use tokio::sync::OnceCell; + +use crate::app_config::Config; + +pub type Result = std::result::Result; +pub type Error = StatusError; +const STEAM_DISCOVERY: &str = "https://steamcommunity.com/openid/"; +static OPENID_INFO: OnceCell = OnceCell::const_new(); + +struct Info { + r#type: String, + uri: String, +} + +#[derive(Debug, Snafu)] +#[non_exhaustive] +#[snafu(visibility(pub(crate)))] +enum InnerError { + QueryingDiscovery, + DiscoveryBadResponse, + DeserializingDiscovery, + InfoAlreadySet, + ExpectedInfoReady, + BuildingURI, + SelfValidationFailed, + PeerValidationFailed, +} + +impl InnerError { + pub fn status_code(&self) -> StatusCode { + match self { + InnerError::QueryingDiscovery => StatusCode::INTERNAL_SERVER_ERROR, + InnerError::DiscoveryBadResponse => StatusCode::INTERNAL_SERVER_ERROR, + InnerError::DeserializingDiscovery => StatusCode::INTERNAL_SERVER_ERROR, + InnerError::InfoAlreadySet => StatusCode::INTERNAL_SERVER_ERROR, + InnerError::ExpectedInfoReady => StatusCode::INTERNAL_SERVER_ERROR, + InnerError::BuildingURI => StatusCode::INTERNAL_SERVER_ERROR, + InnerError::SelfValidationFailed => StatusCode::FORBIDDEN, + InnerError::PeerValidationFailed => StatusCode::FORBIDDEN, + } + } +} + +impl From for StatusError { + fn from(value: InnerError) -> Self { + let mut error = StatusError::internal_server_error(); + error.code = value.status_code(); + error.name = value + .status_code() + .canonical_reason() + .unwrap_or_default() + .to_string(); + error.brief = value.to_string(); + error.detail = value.backtrace().map(std::string::ToString::to_string); + error + } +} + +pub async fn get_url(client: Client, config: &Config) -> Result { + if !OPENID_INFO.initialized() { + let response = client + .get(STEAM_DISCOVERY) + .send() + .await + .map_err(|_| InnerError::QueryingDiscovery)?; + let response_text = response + .text() + .await + .map_err(|_| InnerError::DiscoveryBadResponse)?; + + let doc: Xrds = from_str(&response_text).map_err(|_| InnerError::DeserializingDiscovery)?; + OPENID_INFO + .set(Info { + r#type: doc.xrd.service.r#type, + uri: doc.xrd.service.uri, + }) + .map_err(|_| InnerError::InfoAlreadySet)?; + } + let info = OPENID_INFO.get().ok_or(InnerError::ExpectedInfoReady)?; + let mut url = Url::from_str(&info.uri).map_err(|_| InnerError::BuildingURI)?; + url.query_pairs_mut() + .append_pair("openid.mode", "checkid_setup") + .append_pair("openid.ns", "http://specs.openid.net/auth/2.0") + .append_pair( + "openid.claimed_id", + "http://specs.openid.net/auth/2.0/identifier_select", + ) + .append_pair( + "openid.identity", + "http://specs.openid.net/auth/2.0/identifier_select", + ) + .append_pair("openid.return_to", &redirect_url(&config.base_url)) + .append_pair("openid.realm", &redirect_url(&config.base_url)) + .finish(); + + Ok(url.to_string()) +} + +fn redirect_url(base: &Arc)-> String{ + String::clone(base) + "/api/verify" +} + +#[handler] +pub async fn redirect(resp: &mut Response, depot: &mut Depot) -> Result<()> { + let client = reqwest::Client::new(); + let config = depot.obtain::>().expect("getting shared state"); + let url = get_url(client, config).await?; + resp.render(Redirect::found(url)); + + Ok(()) +} + +#[handler] +pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depot) -> Result<()> { + let map = req.queries(); + { + let info = OPENID_INFO.get().ok_or(InnerError::ExpectedInfoReady)?; + if (map.get("openid.ns").map(String::as_str)) + != (Some(&info.r#type[0..info.r#type.len() - b"/server".len()])) + { + return Err(InnerError::SelfValidationFailed)?; + } + + if (map.get("openid.op_endpoint")) != (Some(&info.uri)) { + return Err(InnerError::SelfValidationFailed)?; + } + if let Some((timestamp, _)) = map.get("openid.response_nonce").unwrap().split_once('Z') { + let timestamp = timestamp.parse::().unwrap(); + if timestamp - Utc::now().naive_utc() > TimeDelta::minutes(5) { + return Err(InnerError::SelfValidationFailed)?; + } + } else { + return Err(InnerError::SelfValidationFailed)?; + } + } + + let mut url = Url::from_str("https://steamcommunity.com/openid/login").unwrap(); + + for item in map.get("openid.signed").unwrap().split(',') { + let key = format!("openid.{item}"); + url.query_pairs_mut() + .append_pair(&key, map.get(&key).unwrap()) + .finish(); + } + url.query_pairs_mut() + .append_pair("openid.sig", map.get("openid.sig").unwrap()) + .append_pair("openid.ns", map.get("openid.ns").unwrap()) + .append_pair("openid.mode", "check_authentication") + .finish(); + + let resp = reqwest::get(url).await.unwrap(); + let text = resp.text().await.unwrap(); + + if text != "ns:http://specs.openid.net/auth/2.0\nis_valid:true\n" { + return Err(InnerError::PeerValidationFailed)?; + } + + let user_id = map + .get("openid.identity") + .unwrap() + .split('/') + .next_back() + .unwrap(); + + let config = depot.obtain::>().expect("getting shared state"); + let keypair = &KeyPair::from(&config.biscuit.private_key); + + let biscuit: biscuit_auth::Biscuit = biscuit!( + r#" + user({user_id}); + check if time($time), $time <= {}; + "# + ) + .build(keypair) + .unwrap(); + + let based = biscuit.to_base64().expect("creating token"); + + response.add_cookie( + Cookie::build(("token", based)) + .max_age(Duration::hours(12)) + .http_only(true) + .secure(true) + .same_site(SameSite::Strict) + .path("/") + .build(), + ); + + response.add_cookie( + Cookie::build("token_set") + .max_age(Duration::hours(12)) + .build(), + ); + // let db: &Surreal = DB_POOL.get().expect("Getting db connection"); + Ok(()) +} +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct Xrds { + #[serde(rename = "XRD")] + xrd: Xrd, +} +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct Xrd { + #[serde(rename = "Service")] + service: Service, +} +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct Service { + #[serde(rename = "Type")] + r#type: String, + #[serde(rename = "URI")] + uri: String, +} diff --git a/src/main.rs b/src/main.rs index 7607943..af10f1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::{ fmt::Write, ops::Add, path::PathBuf, + sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -32,10 +33,12 @@ use crate::{ }; mod app_config; +mod auth; mod language; mod model; mod steam; mod web; + pub type Result = std::result::Result; pub type Error = Whatever; #[tokio::main] @@ -174,7 +177,7 @@ async fn main() -> Result<()> { } }); } - web::start(db).await; + web::start(db, Arc::new(settings)).await; Ok(()) } diff --git a/src/web/mod.rs b/src/web/mod.rs index f7e74a5..358c333 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; use itertools::Itertools; use salvo::{ @@ -24,6 +24,8 @@ use tokio::sync::OnceCell; use tracing::{Instrument, info_span, instrument}; use crate::{ + app_config::Config, + auth, language::DetectedLanguage, model::{FullWorkshopItem, OrderBy, WorkshopItem, into_string}, }; @@ -31,11 +33,17 @@ use crate::{ /// Global static DB_POOL: OnceCell> = OnceCell::const_new(); /// Start the webserver returning once it exists -pub async fn start(db: Surreal) { +pub async fn start(db: Surreal, config: Arc) { DB_POOL.get_or_init(|| async { db }).await; let router = Router::new() .push(Router::with_path("api/list").get(list)) - .push(Router::with_path("api/item/{id}").get(get)); + .push(Router::with_path("api/item/{id}").get(get)) + .push( + Router::with_path("api") + .hoop(affix_state::inject(config)) + .push(Router::with_path("login").get(auth::redirect)) + .push(Router::with_path("verify").get(auth::verify)), + ); let doc = OpenApi::new("workshop-walker", "0.0.1").merge_router(&router); let router = router .push(doc.into_router("/api-doc/openapi.json")) @@ -221,7 +229,7 @@ async fn list( alias: Some("tags".into()), }); } - if let Some(OrderBy::Dependents) = order_by{ + if let Some(OrderBy::Dependents) = order_by { stmt.expr.0.push(Field::Single { expr: idiom(" <-item_dependencies.len()") .expect("expanding item_tags idiom") From e9399799a2496949c1114ffc03c3ea54ff631592 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Fri, 13 Jun 2025 14:44:20 +1200 Subject: [PATCH 03/19] feat: Add updated model user submitted data --- .../20250612_150541_users_and_votes.surql | 77 ++++++++++ src/model.rs | 142 ++++++++++++++++-- 2 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 migrations/20250612_150541_users_and_votes.surql diff --git a/migrations/20250612_150541_users_and_votes.surql b/migrations/20250612_150541_users_and_votes.surql new file mode 100644 index 0000000..56426fd --- /dev/null +++ b/migrations/20250612_150541_users_and_votes.surql @@ -0,0 +1,77 @@ +-- ------------------------------ +-- TABLE: apps +-- ------------------------------ + +DEFINE TABLE OVERWRITE apps TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; + +DEFINE FIELD OVERWRITE available ON apps TYPE bool PERMISSIONS FULL; +DEFINE FIELD OVERWRITE banner ON apps TYPE string PERMISSIONS FULL; +DEFINE FIELD OVERWRITE default_tags ON apps TYPE set> PERMISSIONS FULL; +DEFINE FIELD OVERWRITE default_tags[*] ON apps TYPE record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE description ON apps TYPE string PERMISSIONS FULL; +DEFINE FIELD OVERWRITE developer ON apps TYPE string PERMISSIONS FULL; +DEFINE FIELD OVERWRITE enabled ON apps TYPE bool PERMISSIONS FULL; +DEFINE FIELD OVERWRITE name ON apps TYPE string PERMISSIONS FULL; + + + +-- ------------------------------ +-- TABLE: companions +-- ------------------------------ + +DEFINE TABLE OVERWRITE companions TYPE RELATION IN workshop_items OUT workshop_items SCHEMAFULL PERMISSIONS NONE; + +DEFINE FIELD OVERWRITE in ON companions TYPE record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE note ON companions TYPE option PERMISSIONS FULL; +DEFINE FIELD OVERWRITE out ON companions TYPE record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE source ON companions TYPE 'system' | record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE status ON companions TYPE -1 | 0 | 1 DEFAULT 0 PERMISSIONS FULL; +DEFINE FIELD OVERWRITE upvote_count ON companions TYPE int DEFAULT 0 PERMISSIONS FULL; +DEFINE FIELD OVERWRITE vote_count ON companions TYPE int DEFAULT 0 PERMISSIONS FULL; + + + +-- ------------------------------ +-- TABLE: properties +-- ------------------------------ + +DEFINE TABLE OVERWRITE properties TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; + +DEFINE FIELD OVERWRITE `value` ON properties TYPE string PERMISSIONS FULL; +DEFINE FIELD OVERWRITE app_id ON properties TYPE int PERMISSIONS FULL; +DEFINE FIELD OVERWRITE class ON properties TYPE string PERMISSIONS FULL; +DEFINE FIELD OVERWRITE note ON properties TYPE option PERMISSIONS FULL; +DEFINE FIELD OVERWRITE source ON properties TYPE 'system' | record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE status ON properties TYPE -1 | 0 | 1 DEFAULT 0 PERMISSIONS FULL; +DEFINE FIELD OVERWRITE upvote_count ON properties TYPE int DEFAULT 0 PERMISSIONS FULL; +DEFINE FIELD OVERWRITE vote_count ON properties TYPE int DEFAULT 0 PERMISSIONS FULL; +DEFINE FIELD OVERWRITE workshop_item ON properties TYPE record PERMISSIONS FULL; + + + +-- ------------------------------ +-- TABLE: users +-- ------------------------------ + +DEFINE TABLE OVERWRITE users TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; + +DEFINE FIELD OVERWRITE admin ON users TYPE bool PERMISSIONS FULL; +DEFINE FIELD OVERWRITE banned ON users TYPE bool PERMISSIONS FULL; +DEFINE FIELD OVERWRITE id ON users TYPE string PERMISSIONS FULL; +DEFINE FIELD OVERWRITE last_logged_in ON users TYPE datetime PERMISSIONS FULL; + + + +-- ------------------------------ +-- TABLE: votes +-- ------------------------------ + +DEFINE TABLE OVERWRITE votes TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; + +DEFINE FIELD OVERWRITE link ON votes TYPE record | record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE score ON votes TYPE int PERMISSIONS FULL; +DEFINE FIELD OVERWRITE user ON votes TYPE record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE when ON votes TYPE datetime PERMISSIONS FULL; + + + diff --git a/src/model.rs b/src/model.rs index cf675e8..86bf62f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,7 +1,9 @@ use std::fmt::{Display, Formatter}; +use chrono::{DateTime, Utc}; use salvo::prelude::ToSchema; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use surrealdb::{RecordId, RecordIdKey}; use crate::language::DetectedLanguage; @@ -55,19 +57,19 @@ pub struct WorkshopItem { } #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] pub struct FullWorkshopItem { - pub appid: i64, // The steam ID of the app this belongs to - pub author: String, //Authors steam ID - pub dependants: Vec, // A list of dependants found + pub appid: i64, // The steam ID of the app this belongs to + pub author: String, // Authors steam ID + pub dependants: Vec, // A list of dependants found pub dependencies: Vec, // A list of dependencies found - pub description: String, // HTML encoded description from steam - pub id: String, // The item's ID - pub languages: Vec, // All languages found in the items description - pub last_updated: u64, // Timestamp in milliseconds + pub description: String, // HTML encoded description from steam + pub id: String, // The item's ID + pub languages: Vec, // All languages found in the items description + pub last_updated: u64, // Timestamp in milliseconds #[serde(skip_serializing_if = "Option::is_none")] pub preview_url: Option, // The URL to the banner image - pub title: String, // The titles name - pub tags: Vec, // The list of tags found - pub score: f32, // The "quality" score assigned by steam + pub title: String, // The titles name + pub tags: Vec, // The list of tags found + pub score: f32, // The "quality" score assigned by steam } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Dependencies { @@ -81,3 +83,123 @@ pub struct Dependencies { pub fn into_string(key: &RecordIdKey) -> String { key.to_string().replace("⟩", "").replace("⟨", "") } + +/// A steam workshop app +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct App { + /// The steam ID for an app + pub id: u32, + /// App name, I.E. Rimworld + pub name: String, + /// The developers primary name I.E. Ludeon Studios + pub developer: String, + pub description: String, + /// Banner image URL + pub banner: String, + /// Can the app be interacted with for facets, votes & companions + pub enabled: bool, + /// Whether the app is visible on the index + pub available: bool, + /// List of tags to select by default + pub default_tags: Vec<()>, +} + +/// A workshop walker user +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct User { + /// The steam account ID + pub id: u64, + /// Privileged access + pub admin: bool, + pub banned: bool, + /// UTC timestamp of when the user last logged in + pub last_logged_in: DateTime, +} + +/// Crowdsourced metadata for an item, private version +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Property { + /// Snowflake generated ID + pub id: String, + /// Associated app ID, for enforcing uniqueness + pub app_id: u32, + pub class: Class, + pub value: String, + /// Reasoning or justification for an inclusion + pub note: Option, + pub status: Status, + pub upvote_count: u64, + pub vote_count: u64, + /// The item that this is associated with + pub workshop_item: RecordId, +} + +/// Crowdsourced relationships for an item, used for "soft" dependencies not +/// supplied by steam, private version +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Companion { + /// Snowflake generated ID + pub id: String, + pub r#in: RecordId, + pub out: RecordId, + /// Reasoning or justification for an inclusion + pub note: Option, + pub status: Status, + pub upvote_count: u64, + pub vote_count: u64, + /// The item that this is associated with + pub workshop_item: RecordId, +} + +/// A voting record +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Vote { + /// The app this is associated with, for possible filtering + pub app_id: String, + pub link: RecordId, + pub score: f32, + pub user: RecordId, + pub when: DateTime, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum Source { + /// Auto-generated + System, + /// User submitted + User(RecordId), +} + +#[derive(Debug, ToSchema, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub enum Class { + /// Anything like addon, overhaul, bugfix, patch + r#Type, + /// Literary themes like mecha + Theme, + /// Literary genres like `CyberPunk` + Genre, + /// Mod features, like "new scenario" or "new clothes" + Feature, +} + +#[derive( + Debug, + Default, + ToSchema, + Copy, + Clone, + Serialize_repr, + Deserialize_repr, + Eq, + PartialEq, + Ord, + PartialOrd, +)] +#[repr(i8)] +pub enum Status { + Rejected = -1, + #[default] + Pending = 0, + Accepted = 1, +} From 94d69c0289a3c8276e4099b0674da0674e69458b Mon Sep 17 00:00:00 2001 From: James Kerr Date: Fri, 13 Jun 2025 15:36:17 +1200 Subject: [PATCH 04/19] feat: Add users to the database --- src/auth.rs | 35 +++++++++++++++++++++++++++++++---- src/model.rs | 15 +++++++++++++-- src/web/mod.rs | 3 ++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index aa86fb4..b685093 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -11,9 +11,15 @@ use salvo::{ use serde::{Deserialize, Serialize}; use serde_xml_rs::from_str; use snafu::{ErrorCompat, prelude::*}; +use surrealdb::{ + Surreal, + engine::local::Db, + sql::{Data, Operator, Value, statements::InsertStatement, to_value}, + syn::idiom, +}; use tokio::sync::OnceCell; - -use crate::app_config::Config; +use tracing::error; +use crate::{app_config::Config, model::User}; pub type Result = std::result::Result; pub type Error = StatusError; @@ -109,7 +115,7 @@ pub async fn get_url(client: Client, config: &Config) -> Result { Ok(url.to_string()) } -fn redirect_url(base: &Arc)-> String{ +fn redirect_url(base: &Arc) -> String { String::clone(base) + "/api/verify" } @@ -204,7 +210,28 @@ pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depo .max_age(Duration::hours(12)) .build(), ); - // let db: &Surreal = DB_POOL.get().expect("Getting db connection"); + { + let db = depot.obtain::>().expect("getting shared state"); + let user = User { + id: user_id.to_owned(), + admin: false, + banned: false, + last_logged_in: Utc::now(), + }; + let mut stmt = InsertStatement::default(); + stmt.into = Some(Value::Table("users".into())); + stmt.data = Data::SingleExpression(to_value(user.clone()).unwrap()); + stmt.update = Some(Data::UpdateExpression(vec![( + idiom("last_logged_in").unwrap(), + Operator::Equal, + Utc::now().into(), + )])); + let errors = db.query(stmt).await.unwrap().take_errors(); + for (i, error) in errors{ + error!("Error: {i}: {error}"); + } + } + Ok(()) } #[derive(Debug, Serialize, Deserialize, PartialEq)] diff --git a/src/model.rs b/src/model.rs index 86bf62f..5ea7674 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter}; use chrono::{DateTime, Utc}; use salvo::prelude::ToSchema; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; use serde_repr::{Deserialize_repr, Serialize_repr}; use surrealdb::{RecordId, RecordIdKey}; @@ -108,13 +108,24 @@ pub struct App { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct User { /// The steam account ID - pub id: u64, + pub id: String, /// Privileged access pub admin: bool, pub banned: bool, /// UTC timestamp of when the user last logged in + // Surrealdb bug: https://github.com/surrealdb/surrealdb/issues/3550 + #[serde(serialize_with = "serialize_chrono_as_sql_datetime")] pub last_logged_in: DateTime, } +pub fn serialize_chrono_as_sql_datetime( + x: &chrono::DateTime, + s: S, +) -> Result +where + S: Serializer, +{ + Into::::into(*x).serialize(s) +} /// Crowdsourced metadata for an item, private version #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/src/web/mod.rs b/src/web/mod.rs index 358c333..ec87c16 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -34,13 +34,14 @@ use crate::{ static DB_POOL: OnceCell> = OnceCell::const_new(); /// Start the webserver returning once it exists pub async fn start(db: Surreal, config: Arc) { - DB_POOL.get_or_init(|| async { db }).await; + let db = DB_POOL.get_or_init(|| async { db }).await.clone(); let router = Router::new() .push(Router::with_path("api/list").get(list)) .push(Router::with_path("api/item/{id}").get(get)) .push( Router::with_path("api") .hoop(affix_state::inject(config)) + .hoop(affix_state::inject(db)) .push(Router::with_path("login").get(auth::redirect)) .push(Router::with_path("verify").get(auth::verify)), ); From 361b549cad40157966a5020aaed65db519f22162 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Fri, 13 Jun 2025 17:17:29 +1200 Subject: [PATCH 05/19] feat(UI): Add user login --- ui/src/routes/+layout.svelte | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index cca58e5..0b435eb 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -1,12 +1,19 @@
@@ -20,9 +27,23 @@ {/snippet} Workshop Walker {#snippet trail()} + {#if logged_in} + + Sign Out + + {:else} + + Sign In Through Steam + + {/if} + + {/snippet} From af24966961b6dce33a4c1535e5660920f54ebe45 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Fri, 13 Jun 2025 17:18:02 +1200 Subject: [PATCH 06/19] feat(Backend): Add logout and login redirect support --- src/auth.rs | 38 +++++++++++++++++++++++++++++--------- src/web/mod.rs | 3 ++- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index b685093..d9b76dd 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,7 +5,10 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; use reqwest::{Client, Url}; use salvo::{ Depot, Request, Response, handler, - http::cookie::{Cookie, SameSite, time::Duration}, + http::{ + HeaderValue, + cookie::{Cookie, SameSite, time::Duration}, + }, prelude::{Redirect, StatusCode, StatusError}, }; use serde::{Deserialize, Serialize}; @@ -19,6 +22,7 @@ use surrealdb::{ }; use tokio::sync::OnceCell; use tracing::error; + use crate::{app_config::Config, model::User}; pub type Result = std::result::Result; @@ -75,7 +79,7 @@ impl From for StatusError { } } -pub async fn get_url(client: Client, config: &Config) -> Result { +pub async fn get_url(client: Client, config: &Config, location: &str) -> Result { if !OPENID_INFO.initialized() { let response = client .get(STEAM_DISCOVERY) @@ -108,22 +112,25 @@ pub async fn get_url(client: Client, config: &Config) -> Result { "openid.identity", "http://specs.openid.net/auth/2.0/identifier_select", ) - .append_pair("openid.return_to", &redirect_url(&config.base_url)) - .append_pair("openid.realm", &redirect_url(&config.base_url)) + .append_pair( + "openid.return_to", + &redirect_url(&config.base_url, location), + ) + .append_pair("openid.realm", config.base_url.as_str()) .finish(); Ok(url.to_string()) } -fn redirect_url(base: &Arc) -> String { - String::clone(base) + "/api/verify" +fn redirect_url(base: &Arc, location: &str) -> String { + String::clone(base) + "/api/verify?location=" + location } #[handler] -pub async fn redirect(resp: &mut Response, depot: &mut Depot) -> Result<()> { +pub async fn redirect(req: &mut Request, resp: &mut Response, depot: &mut Depot) -> Result<()> { let client = reqwest::Client::new(); let config = depot.obtain::>().expect("getting shared state"); - let url = get_url(client, config).await?; + let url = get_url(client, config, req.query("location").unwrap()).await?; resp.render(Redirect::found(url)); Ok(()) @@ -208,6 +215,9 @@ pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depo response.add_cookie( Cookie::build("token_set") .max_age(Duration::hours(12)) + .secure(true) + .same_site(SameSite::Strict) + .path("/") .build(), ); { @@ -227,13 +237,23 @@ pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depo Utc::now().into(), )])); let errors = db.query(stmt).await.unwrap().take_errors(); - for (i, error) in errors{ + for (i, error) in errors { error!("Error: {i}: {error}"); } } + response.render(Redirect::found(req.query::<&str>("location").unwrap())); Ok(()) } +#[handler] +pub async fn invalidate(req: &mut Request, response: &mut Response) -> Result<()> { + response + .headers + .insert("Clear-Site-Data", HeaderValue::from_static("\"cookies\"")); + response.render(Redirect::found(req.query::<&str>("location").unwrap())); + Ok(()) +} + #[derive(Debug, Serialize, Deserialize, PartialEq)] struct Xrds { #[serde(rename = "XRD")] diff --git a/src/web/mod.rs b/src/web/mod.rs index ec87c16..1605654 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -43,7 +43,8 @@ pub async fn start(db: Surreal, config: Arc) { .hoop(affix_state::inject(config)) .hoop(affix_state::inject(db)) .push(Router::with_path("login").get(auth::redirect)) - .push(Router::with_path("verify").get(auth::verify)), + .push(Router::with_path("verify").get(auth::verify)) + .push(Router::with_path("logout").get(auth::invalidate)), ); let doc = OpenApi::new("workshop-walker", "0.0.1").merge_router(&router); let router = router From 9b873910bf6f165d47c752ae8b18bf03f445f74f Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:21:46 +1200 Subject: [PATCH 07/19] style(UI): New theme --- ui/src/app.css | 3 +- ui/src/app.html | 2 +- ui/src/workshop-walker.css | 206 +++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 ui/src/workshop-walker.css diff --git a/ui/src/app.css b/ui/src/app.css index 0806158..ab2978a 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -1,7 +1,8 @@ @import 'tailwindcss'; @import '@skeletonlabs/skeleton'; @import '@skeletonlabs/skeleton/optional/presets'; -@import '@skeletonlabs/skeleton/themes/cerberus'; +@import '@skeletonlabs/skeleton/themes/hamlindigo'; +@import '../src/workshop-walker.css'; @plugin '@tailwindcss/forms'; @plugin '@tailwindcss/typography'; @source '../node_modules/@skeletonlabs/skeleton-svelte/dist'; diff --git a/ui/src/app.html b/ui/src/app.html index 7267b06..8e66d3b 100644 --- a/ui/src/app.html +++ b/ui/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/ui/src/workshop-walker.css b/ui/src/workshop-walker.css new file mode 100644 index 0000000..49984d3 --- /dev/null +++ b/ui/src/workshop-walker.css @@ -0,0 +1,206 @@ +[data-theme='hamlindigo'] { + --text-scaling: 1.067; + --base-font-color: var(--color-surface-950); + --base-font-color-dark: var(--color-surface-50); + --base-font-family: + Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif; + --base-font-size: inherit; + --base-line-height: inherit; + --base-font-weight: normal; + --base-font-style: normal; + --base-letter-spacing: 0em; + --heading-font-color: inherit; + --heading-font-color-dark: inherit; + --heading-font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif; + --heading-font-weight: bold; + --heading-font-style: normal; + --heading-letter-spacing: inherit; + --anchor-font-color: var(--color-secondary-500); + --anchor-font-color-dark: var(--color-secondary-500); + --anchor-font-family: inherit; + --anchor-font-size: inherit; + --anchor-line-height: inherit; + --anchor-font-weight: inherit; + --anchor-font-style: inherit; + --anchor-letter-spacing: inherit; + --anchor-text-decoration: none; + --anchor-text-decoration-hover: underline; + --anchor-text-decoration-active: none; + --anchor-text-decoration-focus: none; + --spacing: 0.25rem; + --radius-base: 0.375rem; + --radius-container: 0.75rem; + --default-border-width: 1px; + --default-divide-width: 1px; + --default-ring-width: 1px; + --body-background-color: oklch(1 0 0 / 1); + --body-background-color-dark: var(--color-surface-950); + --color-primary-50: oklch(95.55% 0.02 270.24deg); + --color-primary-100: oklch(92.48% 0.03 267.98deg); + --color-primary-200: oklch(89.48% 0.04 268.21deg); + --color-primary-300: oklch(86.37% 0.05 266.8deg); + --color-primary-400: oklch(83.37% 0.07 267.02deg); + --color-primary-500: oklch(80.3% 0.08 266.38deg); + --color-primary-600: oklch(73.57% 0.07 265.91deg); + --color-primary-700: oklch(66.75% 0.06 266.22deg); + --color-primary-800: oklch(59.42% 0.06 266.62deg); + --color-primary-900: oklch(52.2% 0.05 267.21deg); + --color-primary-950: oklch(44.64% 0.04 266.45deg); + --color-primary-contrast-dark: oklch(0% 0 none); + --color-primary-contrast-light: oklch(100% 0 none); + --color-primary-contrast-50: var(--color-primary-contrast-dark); + --color-primary-contrast-100: var(--color-primary-contrast-dark); + --color-primary-contrast-200: var(--color-primary-contrast-dark); + --color-primary-contrast-300: var(--color-primary-contrast-dark); + --color-primary-contrast-400: var(--color-primary-contrast-dark); + --color-primary-contrast-500: var(--color-primary-contrast-dark); + --color-primary-contrast-600: var(--color-primary-contrast-dark); + --color-primary-contrast-700: var(--color-primary-contrast-dark); + --color-primary-contrast-800: var(--color-primary-contrast-dark); + --color-primary-contrast-900: var(--color-primary-contrast-light); + --color-primary-contrast-950: var(--color-primary-contrast-light); + --color-secondary-50: oklch(97.88% 0.03 92.29deg); + --color-secondary-100: oklch(91.67% 0.04 90.06deg); + --color-secondary-200: oklch(85.27% 0.05 90.4deg); + --color-secondary-300: oklch(78.66% 0.05 87.37deg); + --color-secondary-400: oklch(72.04% 0.06 87.89deg); + --color-secondary-500: oklch(65.42% 0.07 86.84deg); + --color-secondary-600: oklch(60.01% 0.06 86.64deg); + --color-secondary-700: oklch(54.38% 0.06 87.6deg); + --color-secondary-800: oklch(48.68% 0.05 88deg); + --color-secondary-900: oklch(42.72% 0.05 89.52deg); + --color-secondary-950: oklch(36.65% 0.04 89.73deg); + --color-secondary-contrast-dark: oklch(0% 0 none); + --color-secondary-contrast-light: oklch(100% 0 none); + --color-secondary-contrast-50: var(--color-secondary-contrast-dark); + --color-secondary-contrast-100: var(--color-secondary-contrast-dark); + --color-secondary-contrast-200: var(--color-secondary-contrast-dark); + --color-secondary-contrast-300: var(--color-secondary-contrast-dark); + --color-secondary-contrast-400: var(--color-secondary-contrast-dark); + --color-secondary-contrast-500: var(--color-secondary-contrast-dark); + --color-secondary-contrast-600: var(--color-secondary-contrast-dark); + --color-secondary-contrast-700: var(--color-secondary-contrast-light); + --color-secondary-contrast-800: var(--color-secondary-contrast-light); + --color-secondary-contrast-900: var(--color-secondary-contrast-light); + --color-secondary-contrast-950: var(--color-secondary-contrast-light); + --color-tertiary-50: oklch(94.49% 0.01 203.97deg); + --color-tertiary-100: oklch(88.53% 0.02 212.48deg); + --color-tertiary-200: oklch(82.6% 0.03 209.54deg); + --color-tertiary-300: oklch(76.52% 0.04 212.69deg); + --color-tertiary-400: oklch(70.5% 0.05 210.99deg); + --color-tertiary-500: oklch(64.32% 0.06 213.24deg); + --color-tertiary-600: oklch(58.99% 0.05 213.74deg); + --color-tertiary-700: oklch(53.51% 0.05 212.62deg); + --color-tertiary-800: oklch(47.98% 0.04 213.6deg); + --color-tertiary-900: oklch(42.18% 0.04 211.98deg); + --color-tertiary-950: oklch(36.22% 0.03 212.7deg); + --color-tertiary-contrast-dark: oklch(0% 0 none); + --color-tertiary-contrast-light: oklch(100% 0 none); + --color-tertiary-contrast-50: var(--color-tertiary-contrast-dark); + --color-tertiary-contrast-100: var(--color-tertiary-contrast-dark); + --color-tertiary-contrast-200: var(--color-tertiary-contrast-dark); + --color-tertiary-contrast-300: var(--color-tertiary-contrast-dark); + --color-tertiary-contrast-400: var(--color-tertiary-contrast-dark); + --color-tertiary-contrast-500: var(--color-tertiary-contrast-dark); + --color-tertiary-contrast-600: var(--color-tertiary-contrast-dark); + --color-tertiary-contrast-700: var(--color-tertiary-contrast-light); + --color-tertiary-contrast-800: var(--color-tertiary-contrast-light); + --color-tertiary-contrast-900: var(--color-tertiary-contrast-light); + --color-tertiary-contrast-950: var(--color-tertiary-contrast-light); + --color-success-50: oklch(94.07% 0.02 181deg); + --color-success-100: oklch(87.5% 0.03 178.76deg); + --color-success-200: oklch(81.08% 0.04 176.08deg); + --color-success-300: oklch(74.29% 0.06 174.15deg); + --color-success-400: oklch(67.79% 0.07 172.4deg); + --color-success-500: oklch(61.04% 0.08 171.18deg); + --color-success-600: oklch(56.08% 0.07 171.23deg); + --color-success-700: oklch(50.72% 0.07 170.98deg); + --color-success-800: oklch(45.52% 0.06 171.01deg); + --color-success-900: oklch(39.84% 0.06 170.64deg); + --color-success-950: oklch(34.29% 0.05 170.58deg); + --color-success-contrast-dark: oklch(0% 0 none); + --color-success-contrast-light: oklch(100% 0 none); + --color-success-contrast-50: var(--color-success-contrast-dark); + --color-success-contrast-100: var(--color-success-contrast-dark); + --color-success-contrast-200: var(--color-success-contrast-dark); + --color-success-contrast-300: var(--color-success-contrast-dark); + --color-success-contrast-400: var(--color-success-contrast-dark); + --color-success-contrast-500: var(--color-success-contrast-dark); + --color-success-contrast-600: var(--color-success-contrast-dark); + --color-success-contrast-700: var(--color-success-contrast-light); + --color-success-contrast-800: var(--color-success-contrast-light); + --color-success-contrast-900: var(--color-success-contrast-light); + --color-success-contrast-950: var(--color-success-contrast-light); + --color-warning-50: oklch(94.76% 0.03 87.52deg); + --color-warning-100: oklch(90.81% 0.05 86.23deg); + --color-warning-200: oklch(87.09% 0.07 86.74deg); + --color-warning-300: oklch(83.35% 0.1 84.88deg); + --color-warning-400: oklch(79.76% 0.11 84.72deg); + --color-warning-500: oklch(76.06% 0.13 83.08deg); + --color-warning-600: oklch(69.66% 0.12 83.37deg); + --color-warning-700: oklch(63.21% 0.11 83.28deg); + --color-warning-800: oklch(56.7% 0.1 84.6deg); + --color-warning-900: oklch(49.89% 0.09 84.56deg); + --color-warning-950: oklch(42.73% 0.08 85.14deg); + --color-warning-contrast-dark: oklch(0% 0 none); + --color-warning-contrast-light: oklch(100% 0 none); + --color-warning-contrast-50: var(--color-warning-contrast-dark); + --color-warning-contrast-100: var(--color-warning-contrast-dark); + --color-warning-contrast-200: var(--color-warning-contrast-dark); + --color-warning-contrast-300: var(--color-warning-contrast-dark); + --color-warning-contrast-400: var(--color-warning-contrast-dark); + --color-warning-contrast-500: var(--color-warning-contrast-dark); + --color-warning-contrast-600: var(--color-warning-contrast-dark); + --color-warning-contrast-700: var(--color-warning-contrast-dark); + --color-warning-contrast-800: var(--color-warning-contrast-dark); + --color-warning-contrast-900: var(--color-warning-contrast-light); + --color-warning-contrast-950: var(--color-warning-contrast-light); + --color-error-50: oklch(95.28% 0.02 358.78deg); + --color-error-100: oklch(87.73% 0.03 356.45deg); + --color-error-200: oklch(80.24% 0.05 358.43deg); + --color-error-300: oklch(72.59% 0.06 358.21deg); + --color-error-400: oklch(64.91% 0.07 359.92deg); + --color-error-500: oklch(57.04% 0.09 0.21deg); + --color-error-600: oklch(52.36% 0.08 1.02deg); + --color-error-700: oklch(47.29% 0.08 359.76deg); + --color-error-800: oklch(42.38% 0.07 0.75deg); + --color-error-900: oklch(37.01% 0.06 358.98deg); + --color-error-950: oklch(31.76% 0.05 0.23deg); + --color-error-contrast-dark: oklch(0% 0 none); + --color-error-contrast-light: oklch(100% 0 none); + --color-error-contrast-50: var(--color-error-contrast-dark); + --color-error-contrast-100: var(--color-error-contrast-dark); + --color-error-contrast-200: var(--color-error-contrast-dark); + --color-error-contrast-300: var(--color-error-contrast-dark); + --color-error-contrast-400: var(--color-error-contrast-dark); + --color-error-contrast-500: var(--color-error-contrast-light); + --color-error-contrast-600: var(--color-error-contrast-light); + --color-error-contrast-700: var(--color-error-contrast-light); + --color-error-contrast-800: var(--color-error-contrast-light); + --color-error-contrast-900: var(--color-error-contrast-light); + --color-error-contrast-950: var(--color-error-contrast-light); + --color-surface-50: oklch(83.05% 0.07 225.68deg); + --color-surface-100: oklch(75.52% 0.07 225.51deg); + --color-surface-200: oklch(68.1% 0.07 224.38deg); + --color-surface-300: oklch(60.2% 0.07 223.41deg); + --color-surface-400: oklch(52.43% 0.07 222.7deg); + --color-surface-500: oklch(44.18% 0.07 223.86deg); + --color-surface-600: oklch(38.1% 0.06 225.02deg); + --color-surface-700: oklch(31.76% 0.05 226.89deg); + --color-surface-800: oklch(24.81% 0.04 235.01deg); + --color-surface-900: oklch(17.63% 0.03 244.21deg); + --color-surface-950: oklch(5.81% 0.04 264.05deg); + --color-surface-contrast-dark: oklch(0% 0 none); + --color-surface-contrast-light: oklch(100% 0 none); + --color-surface-contrast-50: var(--color-surface-contrast-dark); + --color-surface-contrast-100: var(--color-surface-contrast-dark); + --color-surface-contrast-200: var(--color-surface-contrast-dark); + --color-surface-contrast-300: var(--color-surface-contrast-dark); + --color-surface-contrast-400: var(--color-surface-contrast-light); + --color-surface-contrast-500: var(--color-surface-contrast-light); + --color-surface-contrast-600: var(--color-surface-contrast-light); + --color-surface-contrast-700: var(--color-surface-contrast-light); + --color-surface-contrast-800: var(--color-surface-contrast-light); + --color-surface-contrast-900: var(--color-surface-contrast-light); + --color-surface-contrast-950: var(--color-surface-contrast-light); +} From 08bc21eac6aad187ff3ba05bba5414a23935123e Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:24:06 +1200 Subject: [PATCH 08/19] feat: restructure properties and add id fields - Added id fields to apps, companions, and votes tables - Restructured properties table to use an object id (class+value) - Created new workshop_item_properties relation table - Moved property-related fields from properties to workshop_item_properties - Added unique index on workshop_item_properties (in, out) --- .../20250612_150541_users_and_votes.surql | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/migrations/20250612_150541_users_and_votes.surql b/migrations/20250612_150541_users_and_votes.surql index 56426fd..8f51507 100644 --- a/migrations/20250612_150541_users_and_votes.surql +++ b/migrations/20250612_150541_users_and_votes.surql @@ -4,6 +4,7 @@ DEFINE TABLE OVERWRITE apps TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; +DEFINE FIELD OVERWRITE id ON apps TYPE int PERMISSIONS FULL; DEFINE FIELD OVERWRITE available ON apps TYPE bool PERMISSIONS FULL; DEFINE FIELD OVERWRITE banner ON apps TYPE string PERMISSIONS FULL; DEFINE FIELD OVERWRITE default_tags ON apps TYPE set> PERMISSIONS FULL; @@ -21,6 +22,7 @@ DEFINE FIELD OVERWRITE name ON apps TYPE string PERMISSIONS FULL; DEFINE TABLE OVERWRITE companions TYPE RELATION IN workshop_items OUT workshop_items SCHEMAFULL PERMISSIONS NONE; +DEFINE FIELD OVERWRITE id ON companions TYPE int PERMISSIONS FULL; DEFINE FIELD OVERWRITE in ON companions TYPE record PERMISSIONS FULL; DEFINE FIELD OVERWRITE note ON companions TYPE option PERMISSIONS FULL; DEFINE FIELD OVERWRITE out ON companions TYPE record PERMISSIONS FULL; @@ -31,21 +33,14 @@ DEFINE FIELD OVERWRITE vote_count ON companions TYPE int DEFAULT 0 PERMISSIONS F + -- ------------------------------ -- TABLE: properties -- ------------------------------ DEFINE TABLE OVERWRITE properties TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; -DEFINE FIELD OVERWRITE `value` ON properties TYPE string PERMISSIONS FULL; -DEFINE FIELD OVERWRITE app_id ON properties TYPE int PERMISSIONS FULL; -DEFINE FIELD OVERWRITE class ON properties TYPE string PERMISSIONS FULL; -DEFINE FIELD OVERWRITE note ON properties TYPE option PERMISSIONS FULL; -DEFINE FIELD OVERWRITE source ON properties TYPE 'system' | record PERMISSIONS FULL; -DEFINE FIELD OVERWRITE status ON properties TYPE -1 | 0 | 1 DEFAULT 0 PERMISSIONS FULL; -DEFINE FIELD OVERWRITE upvote_count ON properties TYPE int DEFAULT 0 PERMISSIONS FULL; -DEFINE FIELD OVERWRITE vote_count ON properties TYPE int DEFAULT 0 PERMISSIONS FULL; -DEFINE FIELD OVERWRITE workshop_item ON properties TYPE record PERMISSIONS FULL; +DEFINE FIELD id ON properties TYPE { class: string, value: string } PERMISSIONS FULL; @@ -68,10 +63,24 @@ DEFINE FIELD OVERWRITE last_logged_in ON users TYPE datetime PERMISSIONS FULL; DEFINE TABLE OVERWRITE votes TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; +DEFINE FIELD OVERWRITE id ON votes TYPE int PERMISSIONS FULL; DEFINE FIELD OVERWRITE link ON votes TYPE record | record PERMISSIONS FULL; DEFINE FIELD OVERWRITE score ON votes TYPE int PERMISSIONS FULL; DEFINE FIELD OVERWRITE user ON votes TYPE record PERMISSIONS FULL; DEFINE FIELD OVERWRITE when ON votes TYPE datetime PERMISSIONS FULL; +-- ------------------------------ +-- TABLE: workshop_item_properties +-- ------------------------------ + +DEFINE TABLE OVERWRITE workshop_item_properties TYPE RELATION IN workshop_items OUT properties SCHEMAFULL PERMISSIONS NONE; +DEFINE FIELD OVERWRITE in ON workshop_item_properties TYPE record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE note ON workshop_item_properties TYPE option PERMISSIONS FULL; +DEFINE FIELD OVERWRITE out ON workshop_item_properties TYPE record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE source ON workshop_item_properties TYPE 'system' | record PERMISSIONS FULL; +DEFINE FIELD OVERWRITE status ON workshop_item_properties TYPE -1 | 0 | 1 DEFAULT 0 PERMISSIONS FULL; +DEFINE FIELD OVERWRITE upvote_count ON workshop_item_properties TYPE int DEFAULT 0 PERMISSIONS FULL; +DEFINE FIELD OVERWRITE vote_count ON workshop_item_properties TYPE int DEFAULT 0 PERMISSIONS FULL; +DEFINE INDEX unique_workshop_item_properties ON workshop_item_properties FIELDS in, out UNIQUE; From 543b9564b1dbce5be6b199cea7ba2a6f3ddc3350 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:29:15 +1200 Subject: [PATCH 09/19] feat(backend): Add auth verification --- src/auth.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index d9b76dd..b5dde25 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,15 +1,20 @@ -use std::{str::FromStr, sync::Arc}; +use std::{str::FromStr, sync::Arc, time::SystemTime}; -use biscuit_auth::{KeyPair, macros::biscuit}; +use biscuit_auth::{ + Authorizer, Biscuit, KeyPair, + builder_ext::AuthorizerExt, + macros::{authorizer, biscuit}, +}; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use itertools::Itertools; use reqwest::{Client, Url}; use salvo::{ - Depot, Request, Response, handler, + Depot, Request, Response, http::{ HeaderValue, cookie::{Cookie, SameSite, time::Duration}, }, - prelude::{Redirect, StatusCode, StatusError}, + prelude::{Redirect, StatusCode, StatusError, endpoint}, }; use serde::{Deserialize, Serialize}; use serde_xml_rs::from_str; @@ -21,9 +26,9 @@ use surrealdb::{ syn::idiom, }; use tokio::sync::OnceCell; -use tracing::error; +use tracing::{debug, error}; -use crate::{app_config::Config, model::User}; +use crate::{app_config::Config, db::UserID, model::User}; pub type Result = std::result::Result; pub type Error = StatusError; @@ -126,7 +131,7 @@ fn redirect_url(base: &Arc, location: &str) -> String { String::clone(base) + "/api/verify?location=" + location } -#[handler] +#[endpoint] pub async fn redirect(req: &mut Request, resp: &mut Response, depot: &mut Depot) -> Result<()> { let client = reqwest::Client::new(); let config = depot.obtain::>().expect("getting shared state"); @@ -136,7 +141,7 @@ pub async fn redirect(req: &mut Request, resp: &mut Response, depot: &mut Depot) Ok(()) } -#[handler] +#[endpoint] pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depot) -> Result<()> { let map = req.queries(); { @@ -194,8 +199,9 @@ pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depo let biscuit: biscuit_auth::Biscuit = biscuit!( r#" user({user_id}); - check if time($time), $time <= {}; - "# + check if time($time), $time <= {expires}; + "#, + expires = SystemTime::now() + Duration::hours(12) ) .build(keypair) .unwrap(); @@ -223,7 +229,7 @@ pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depo { let db = depot.obtain::>().expect("getting shared state"); let user = User { - id: user_id.to_owned(), + id: UserID::from(user_id.to_owned()).into_recordid(), admin: false, banned: false, last_logged_in: Utc::now(), @@ -245,7 +251,8 @@ pub async fn verify(req: &mut Request, response: &mut Response, depot: &mut Depo response.render(Redirect::found(req.query::<&str>("location").unwrap())); Ok(()) } -#[handler] + +#[endpoint] pub async fn invalidate(req: &mut Request, response: &mut Response) -> Result<()> { response .headers @@ -254,6 +261,77 @@ pub async fn invalidate(req: &mut Request, response: &mut Response) -> Result<() Ok(()) } +#[endpoint] +pub async fn validate(req: &mut Request, depot: &mut Depot, response: &mut Response) { + match req.cookie("token") { + None => { + response.status_code(StatusCode::UNAUTHORIZED); + } + Some(token) => { + let config = depot.obtain::>().expect("getting shared state"); + let keypair = &KeyPair::from(&config.biscuit.private_key); + let token = Biscuit::from_base64(token.value(), keypair.public()).unwrap(); + let mut authorizer: Authorizer = + authorizer!("").time().allow_all().build(&token).unwrap(); + + if let Err(e) = authorizer.authorize() { + debug!("Auth failed: {e:?}"); + response.status_code(StatusCode::UNAUTHORIZED); + return; + } + + depot.inject(authorizer); + } + } +} + +#[endpoint] +pub async fn enforce_admin(depot: &mut Depot, response: &mut Response) { + match get_user(depot).await { + None => { + response.status_code(StatusCode::UNAUTHORIZED); + } + Some(userid) => { + let result = depot + .obtain::>() + .expect("getting shared state") + .query("SElECT admin FROM $user") + .bind(("user", UserID::from(userid).into_recordid())) + .await; + match result.map(surrealdb::Response::check) { + Err(e) => { + error!("running admin query: {e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + } + Ok(Err(e)) => { + error!("checking admin query: {e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + } + Ok(Ok(mut db_response)) => { + let is_admin: Option = db_response.take("admin").unwrap(); + if is_admin != Some(true) { + response.status_code(StatusCode::UNAUTHORIZED); + } + } + } + } + } +} +#[endpoint] +pub async fn validate_opt(req: &mut Request, depot: &mut Depot, response: &mut Response) { + if req.cookie("token").is_some() { + validate::validate(req, depot, response).await; + } +} + +pub async fn get_user(depot: &mut Depot) -> Option { + let authorizer = depot.obtain_mut::().ok()?; + let (userid, _): (String, i64) = authorizer + .query_exactly_one("data($user, 0) <- user($user)") + .ok()?; + Some(userid) +} + #[derive(Debug, Serialize, Deserialize, PartialEq)] struct Xrds { #[serde(rename = "XRD")] From cc13b39adaf168a5852f330612bf529517553e82 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:29:53 +1200 Subject: [PATCH 10/19] chore: Scaffold out companion support --- src/web/companions.rs | 7 ++ ui/src/routes/item/[item]/Companion.svelte | 85 ++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/web/companions.rs create mode 100644 ui/src/routes/item/[item]/Companion.svelte diff --git a/src/web/companions.rs b/src/web/companions.rs new file mode 100644 index 0000000..7e08fdf --- /dev/null +++ b/src/web/companions.rs @@ -0,0 +1,7 @@ +use salvo::prelude::endpoint; +#[endpoint] +pub async fn vote() {} +#[endpoint] +pub async fn remove() {} +#[endpoint] +pub async fn new() {} diff --git a/ui/src/routes/item/[item]/Companion.svelte b/ui/src/routes/item/[item]/Companion.svelte new file mode 100644 index 0000000..ebfcb6a --- /dev/null +++ b/ui/src/routes/item/[item]/Companion.svelte @@ -0,0 +1,85 @@ + + +
+ +
+ + banner +
+ {item.title} +
+
+ + +
+ + + {item.votes ?? 0} + + +
+
+ + +
+ {#each item.tags as tag (tag.id)} + + {tag.display_name} + + {:else} + No tags + {/each} +
+ + + +
From 61d52e641f57d3d0d330863a8006616194e58d98 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:31:12 +1200 Subject: [PATCH 11/19] feat(backend): Add UserID and (workshop) ItemID newtypes --- src/db.rs | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/db.rs diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..a835ebd --- /dev/null +++ b/src/db.rs @@ -0,0 +1,4 @@ +use macros::define_id; + +define_id!("users", UserID, String); +define_id!("workshop_items", ItemID, String); \ No newline at end of file From 07ec4c2f8e325dc825374f0f691ace7cefdba8f8 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:32:18 +1200 Subject: [PATCH 12/19] feat: Add support for `properties` Properties are Type:value pairs that are user submitted and voted upon, this is the initial implementation of this feature. --- Cargo.lock | 1002 ++++++++++------- Cargo.toml | 11 +- src/main.rs | 4 +- src/model.rs | 253 ++++- src/web/admin.rs | 134 +++ src/web/mod.rs | 174 ++- src/web/properties.rs | 310 +++++ ui/src/routes/+layout.svelte | 7 +- ui/src/routes/admin/+page.svelte | 212 ++++ ui/src/routes/admin/+page.ts | 16 + ui/src/routes/app/[id]/+page.svelte | 31 +- ui/src/routes/item/[item]/+page.svelte | 383 ++++++- ui/src/routes/item/[item]/Property.svelte | 112 ++ .../routes/item/[item]/PropertyPrompt.svelte | 138 +++ 14 files changed, 2317 insertions(+), 470 deletions(-) create mode 100644 src/web/admin.rs create mode 100644 src/web/properties.rs create mode 100644 ui/src/routes/admin/+page.svelte create mode 100644 ui/src/routes/admin/+page.ts create mode 100644 ui/src/routes/item/[item]/Property.svelte create mode 100644 ui/src/routes/item/[item]/PropertyPrompt.svelte diff --git a/Cargo.lock b/Cargo.lock index 94be77d..29907e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,9 +32,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -104,6 +104,7 @@ dependencies = [ "const-random", "getrandom 0.3.3", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -140,9 +141,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.1.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ada2ee439075a3e70b6992fce18ac4e407cd05aea9ca3f75d2c0b0c20bbb364" +checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" dependencies = [ "cssparser", "html5ever", @@ -168,9 +169,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -183,33 +184,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -218,9 +219,9 @@ dependencies = [ [[package]] name = "any_ascii" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea50b14b7a4b9343f8c627a7a53c52076482bd4bdad0a24fd3ec533ed616cc2c" +checksum = "90c6333e01ba7235575b6ab53e5af10f1c327927fd97c36462917e289557ea64" [[package]] name = "anyhow" @@ -299,9 +300,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -340,7 +341,7 @@ dependencies = [ "futures-timer", "futures-util", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "mime", "multer", "num-traits", @@ -366,7 +367,7 @@ dependencies = [ "proc-macro2", "quote", "strum 0.26.3", - "syn 2.0.101", + "syn 2.0.104", "thiserror 1.0.69", ] @@ -389,7 +390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_json", ] @@ -413,7 +414,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -430,7 +431,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -452,9 +453,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" @@ -497,9 +498,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bbscope" @@ -550,7 +551,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -568,7 +569,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -739,7 +740,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -765,9 +766,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecheck" @@ -793,9 +794,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" dependencies = [ "bytemuck_derive", ] @@ -808,7 +809,7 @@ checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -845,7 +846,7 @@ dependencies = [ [[package]] name = "candle-core" version = "0.9.1" -source = "git+https://github.com/huggingface/candle.git#0224a749f0b2082f19831256ced6afe284c56457" +source = "git+https://github.com/huggingface/candle.git#be411aa562740c04944097675a2c11d7ed79c72d" dependencies = [ "byteorder", "gemm 0.17.1", @@ -868,7 +869,7 @@ dependencies = [ [[package]] name = "candle-examples" version = "0.9.1" -source = "git+https://github.com/huggingface/candle.git#0224a749f0b2082f19831256ced6afe284c56457" +source = "git+https://github.com/huggingface/candle.git#be411aa562740c04944097675a2c11d7ed79c72d" dependencies = [ "anyhow", "candle-core", @@ -888,7 +889,7 @@ dependencies = [ [[package]] name = "candle-nn" version = "0.9.1" -source = "git+https://github.com/huggingface/candle.git#0224a749f0b2082f19831256ced6afe284c56457" +source = "git+https://github.com/huggingface/candle.git#be411aa562740c04944097675a2c11d7ed79c72d" dependencies = [ "candle-core", "half", @@ -902,7 +903,7 @@ dependencies = [ [[package]] name = "candle-transformers" version = "0.9.1" -source = "git+https://github.com/huggingface/candle.git#0224a749f0b2082f19831256ced6afe284c56457" +source = "git+https://github.com/huggingface/candle.git#be411aa562740c04944097675a2c11d7ed79c72d" dependencies = [ "byteorder", "candle-core", @@ -928,9 +929,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.25" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "jobserver", "libc", @@ -1005,9 +1006,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -1089,9 +1090,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -1099,9 +1100,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -1111,21 +1112,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "classification" @@ -1139,7 +1140,7 @@ dependencies = [ "clap", "hf-hub", "humantime", - "indicatif", + "indicatif 0.17.11", "intel-mkl-src", "serde", "serde_json", @@ -1159,7 +1160,7 @@ dependencies = [ "cli-table-derive", "csv", "termcolor", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -1170,7 +1171,7 @@ checksum = "9f7c1b60bae2c3d45228dfb096046aa51ef6c300de70b658d7a13fcb0c4f832e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1202,9 +1203,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "compact_str" @@ -1232,9 +1233,9 @@ dependencies = [ [[package]] name = "config" -version = "0.15.11" +version = "0.15.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80" +checksum = "5b1eb4fb07bc7f012422df02766c7bd5971effb894f573865642f06fa3265440" dependencies = [ "async-trait", "convert_case", @@ -1244,7 +1245,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml", + "toml 0.9.0", "winnow", "yaml-rust2", ] @@ -1258,10 +1259,23 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.1", + "windows-sys 0.60.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1391,9 +1405,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1438,7 +1452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1473,9 +1487,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.3" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "373b7c5dbd637569a2cca66e8d66b8c446a1e7bf064ea321d265d7b3dfe7c97e" dependencies = [ "cfg-if", "cpufeatures", @@ -1495,7 +1509,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1519,7 +1533,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1530,7 +1544,16 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.101", + "syn 2.0.104", +] + +[[package]] +name = "dary_heap" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" +dependencies = [ + "serde", ] [[package]] @@ -1604,7 +1627,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1625,7 +1648,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1635,7 +1658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1680,16 +1703,16 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", ] [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", ] [[package]] @@ -1710,10 +1733,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[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 0.5.0", + "windows-sys 0.60.2", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1721,7 +1756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -1733,7 +1768,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1755,6 +1790,12 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "double-ended-peekable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d05e1c0dbad51b52c38bda7adceef61b9efc2baf04acfe8726a8c4630a6f57" + [[package]] name = "dtoa" version = "1.0.10" @@ -1770,6 +1811,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + [[package]] name = "dyn-stack" version = "0.10.0" @@ -1826,9 +1873,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", @@ -1905,27 +1952,27 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "enumflags2" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1936,12 +1983,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2045,9 +2092,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "filetime" @@ -2069,9 +2116,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -2226,7 +2273,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2570,7 +2617,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2590,14 +2637,14 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2635,9 +2682,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -2645,7 +2692,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2692,9 +2739,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -2707,16 +2754,16 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.4", ] [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", "headers-core", "http", @@ -2752,9 +2799,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -2764,26 +2811,26 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hf-hub" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc03dcb0b0a83ae3f3363ec811014ae669f083e4e499c66602f447c4828737a1" +checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ "dirs", "futures", "http", - "indicatif", + "indicatif 0.17.11", "libc", "log", "native-tls", "num_cpus", - "rand 0.8.5", + "rand 0.9.1", "reqwest", "serde", "serde_json", "thiserror 2.0.12", "tokio", "ureq", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2806,12 +2853,11 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.31.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", - "mac", "markup5ever", "match_token", ] @@ -2891,9 +2937,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", @@ -2905,7 +2951,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] @@ -2926,9 +2972,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ "base64 0.22.1", "bytes", @@ -3139,12 +3185,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.15.4", "serde", ] @@ -3154,10 +3200,23 @@ version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ - "console", + "console 0.15.11", "number_prefix", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width 0.2.1", + "web-time", +] + +[[package]] +name = "indicatif" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +dependencies = [ + "console 0.16.0", + "portable-atomic", + "unicode-width 0.2.1", + "unit-prefix", "web-time", ] @@ -3201,6 +3260,17 @@ dependencies = [ "rustversion", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3400,9 +3470,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" @@ -3411,7 +3481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.2", ] [[package]] @@ -3422,9 +3492,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmimalloc-sys" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" dependencies = [ "cc", "libc", @@ -3432,9 +3502,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" dependencies = [ "bitflags 2.9.1", "libc", @@ -4281,6 +4351,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.4", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -4305,9 +4384,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "macro_rules_attribute" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a82271f7bc033d84bbca59a3ce3e4159938cb08a9c3aebbe54d215131518a13" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" dependencies = [ "macro_rules_attribute-proc_macro", "paste", @@ -4315,9 +4394,16 @@ dependencies = [ [[package]] name = "macro_rules_attribute-proc_macro" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "serde", +] [[package]] name = "maplit" @@ -4327,9 +4413,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.16.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a8096766c229e8c88a3900c9b44b7e06aa7f7343cc229158c3e58ef8f9973a" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", "tendril", @@ -4338,13 +4424,13 @@ dependencies = [ [[package]] name = "match_token" -version = "0.1.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4378,9 +4464,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" @@ -4412,14 +4498,14 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "mimalloc" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" dependencies = [ "libmimalloc-sys", ] @@ -4458,9 +4544,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -4473,7 +4559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -4495,7 +4581,7 @@ checksum = "c402a4092d5e204f32c9e155431046831fa712637043c58cb73bc6bc6c9663b5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4596,9 +4682,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.1", "cfg-if", @@ -4746,23 +4832,24 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4782,9 +4869,9 @@ dependencies = [ [[package]] name = "object_store" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94ac16b433c0ccf75326388c893d2835ab7457ea35ab8ba5d745c053ef5fa16" +checksum = "7781f96d79ed0f961a7021424ab01840efbda64ae7a505aaea195efc91eaaec4" dependencies = [ "async-trait", "bytes", @@ -4839,7 +4926,7 @@ dependencies = [ "sha2 0.10.9", "tar", "thiserror 1.0.69", - "toml", + "toml 0.8.23", "ureq", "url", "uuid", @@ -4909,7 +4996,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4954,9 +5041,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" dependencies = [ "supports-color 2.1.0", "supports-color 3.0.2", @@ -5077,9 +5164,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -5088,9 +5175,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -5098,24 +5185,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2 0.10.9", ] @@ -5127,7 +5213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.10.0", ] [[package]] @@ -5180,7 +5266,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "unicase", ] @@ -5217,7 +5303,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5285,9 +5371,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" @@ -5356,7 +5442,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5376,7 +5462,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "version_check", "yansi", ] @@ -5487,6 +5573,18 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "quick_cache" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b450dad8382b1b95061d5ca1eb792081fb082adf48c678791fe917509596d5f" +dependencies = [ + "ahash 0.8.12", + "equivalent", + "hashbrown 0.15.4", + "parking_lot", +] + [[package]] name = "quinn" version = "0.11.8" @@ -5530,9 +5628,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", @@ -5553,9 +5651,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -5679,12 +5777,12 @@ dependencies = [ [[package]] name = "rayon-cond" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" dependencies = [ "either", - "itertools 0.11.0", + "itertools 0.14.0", "rayon", ] @@ -5712,9 +5810,9 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags 2.9.1", ] @@ -5730,6 +5828,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.12", +] + [[package]] name = "ref-cast" version = "1.0.24" @@ -5747,7 +5856,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5811,9 +5920,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.18" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", @@ -5828,13 +5937,11 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", @@ -5856,7 +5963,16 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", +] + +[[package]] +name = "revision" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f53179a035f881adad8c4d58a2c599c6b4a8325b989c68d178d7a34d1b1e4c" +dependencies = [ + "revision-derive 0.10.0", ] [[package]] @@ -5868,12 +5984,23 @@ dependencies = [ "chrono", "geo", "regex", - "revision-derive", + "revision-derive 0.11.0", "roaring", "rust_decimal", "uuid", ] +[[package]] +name = "revision-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0ec466e5d8dca9965eb6871879677bef5590cf7525ad96cae14376efb75073" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "revision-derive" version = "0.11.0" @@ -5882,7 +6009,7 @@ checksum = "d3415e1bc838c36f9a0a2ac60c0fa0851c72297685e66592c44870d82834dfa2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6009,6 +6136,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rs-snowflake" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" + [[package]] name = "rstar" version = "0.12.2" @@ -6040,7 +6173,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.101", + "syn 2.0.104", "walkdir", ] @@ -6056,13 +6189,12 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791" dependencies = [ "cfg-if", "ordered-multimap", - "trim-in-place", ] [[package]] @@ -6077,9 +6209,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.37.1" +version = "1.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" dependencies = [ "arrayvec", "borsh", @@ -6093,9 +6225,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -6142,9 +6274,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "log", "once_cell", @@ -6230,9 +6362,9 @@ dependencies = [ [[package]] name = "salvo" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bb64de0061fbaff9be376833e033ee3382cab6c27f52905706ceef873e722" +checksum = "7ce0331005d85590a43295118391452dd04cf7826c2f8251c2362874b8d8b67a" dependencies = [ "salvo-jwt-auth", "salvo-oapi", @@ -6244,9 +6376,9 @@ dependencies = [ [[package]] name = "salvo-jwt-auth" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b43ddd470d57f443c5a391d5805aabd37e4f56599411f51538a5ccdc560fb4c" +checksum = "d9d41ac7983e15ce964f2c2b55d07ea83f1ec8c40abac4fc912ae2c34c272205" dependencies = [ "base64 0.22.1", "bytes", @@ -6264,17 +6396,18 @@ dependencies = [ [[package]] name = "salvo-oapi" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e467e874a74f22743fbfa9a89f4011526be1b298e4f1e0a9c519cac655a846" +checksum = "7b9ef2a41e4bebb38f859d12b257a386e26eb736ad2932b39a7fa6b4a1574cbc" dependencies = [ + "anyhow", "base64 0.22.1", "bytes", "chrono", "compact_str", "futures-util", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "inventory", "mime-infer", "parking_lot", @@ -6298,9 +6431,9 @@ dependencies = [ [[package]] name = "salvo-oapi-macros" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94262c432013ec9c72eccabbec4fb7c148b452739f1407ee0c1f0c2a97f3cd4" +checksum = "e11a94dacba1e1faeeb27c0affcecea7913ae086f9dfc8476dfb115a61c9f412" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6308,14 +6441,14 @@ dependencies = [ "quote", "regex", "salvo-serde-util", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "salvo-proxy" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e90ad085d0b0c08e77234844594ec94230652cdb7245f02490b12659711801e" +checksum = "60554233611342021acaf1340679427e388e358486fa5ef7684d90aa738c854b" dependencies = [ "fastrand", "futures-util", @@ -6331,20 +6464,20 @@ dependencies = [ [[package]] name = "salvo-serde-util" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31ef5876da075021fdb113d40ad8129886eb2e3cdca54e06205b8911ab5a482" +checksum = "2014d193ab04bf917c574c155801279474becd532a02f7dbe64928bdd2b072a4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "salvo-serve-static" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5ea003193ecef2f5839e85e13c0d457677f654ddb3e0bbc69a896ade2627ff" +checksum = "a98e8c27387f5b28ce66c337ae5772268c6d5f728b605fad4355cd4b58f6974b" dependencies = [ "hex", "mime", @@ -6362,9 +6495,9 @@ dependencies = [ [[package]] name = "salvo_core" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37a9f4216b20277baadcc312ab1d8653b62c3ecbc3d39469079054248ccd9b9" +checksum = "fb81e2093c75ff8c3273196c7b5e3b2fb9c5f838ef7e925753ce7e58ddbd6fe4" dependencies = [ "async-trait", "base64 0.22.1", @@ -6380,7 +6513,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap 2.9.0", + "indexmap 2.10.0", "mime", "mime-infer", "multer", @@ -6394,7 +6527,7 @@ dependencies = [ "rustls-pemfile", "salvo_macros", "serde", - "serde-xml-rs 0.6.0", + "serde-xml-rs", "serde_json", "sync_wrapper", "tempfile", @@ -6407,9 +6540,9 @@ dependencies = [ [[package]] name = "salvo_extra" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513ad4320d2245ed5e8f450350a673401ca51e159ee68750afc2914ae9484778" +checksum = "3f90ca584472adbb3dacf4e82bdf46f8022771916342d598b5521f6bfe6b8bfd" dependencies = [ "base64 0.22.1", "etag", @@ -6421,7 +6554,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-tungstenite 0.26.2", + "tokio-tungstenite 0.27.0", "tower", "tracing", "ulid", @@ -6429,16 +6562,16 @@ dependencies = [ [[package]] name = "salvo_macros" -version = "0.78.0" +version = "0.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00a8e1934f0e9f69ed76f30a2cf2b9ca30c61e47e9536d57275c09fd64d4d895" +checksum = "83c13353864ea07e0673511a843b762c775b7f9364657ff5c49ad5fccc5ce0a0" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", "salvo-serde-util", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6459,6 +6592,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -6584,18 +6741,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "serde-xml-rs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" -dependencies = [ - "log", - "serde", - "thiserror 1.0.69", - "xml-rs", -] - [[package]] name = "serde-xml-rs" version = "0.8.1" @@ -6616,7 +6761,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6625,7 +6770,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -6638,7 +6783,7 @@ version = "0.9.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e408f29489b5fd500fab51ff1484fc859bb655f32c671f307dcd733b72e8168c" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -6662,14 +6807,23 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] @@ -6688,15 +6842,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -6706,14 +6862,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6818,18 +6974,15 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smol_str" @@ -6858,7 +7011,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6890,11 +7043,11 @@ dependencies = [ [[package]] name = "spade" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ece03ff43cd2a9b57ebf776ea5e78bd30b3b4185a619f041079f4109f385034" +checksum = "a14e31a007e9f85c32784b04f89e6e194bb252a4d41b4a8ccd9e77245d901c8c" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.4", "num-traits", "robust", "smallvec", @@ -7048,7 +7201,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7061,7 +7214,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7091,9 +7244,9 @@ dependencies = [ [[package]] name = "surrealdb" -version = "2.3.3" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf2d416390d6acd6e37b369805324a27398fa881b2279d85c65f7a857ef72c6" +checksum = "5545940eb21920f4eb3fbdd4a805c68c9917e9ee95b805d7702c0a6cf61ed4d0" dependencies = [ "arrayvec", "async-channel", @@ -7103,12 +7256,12 @@ dependencies = [ "futures", "geo", "getrandom 0.3.3", - "indexmap 2.9.0", + "indexmap 2.10.0", "path-clean", "pharos", "reblessive", "reqwest", - "revision", + "revision 0.11.0", "ring", "rust_decimal", "rustls", @@ -7133,9 +7286,9 @@ dependencies = [ [[package]] name = "surrealdb-core" -version = "2.3.3" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb78cf5031f8a506be71ee978faf541446e879862f727dc372d2227a7e0f585" +checksum = "04c0c3d3e8e8156a1f15e3f146b8e40d7a0197e89abea6c4aa0178d06d11e6d1" dependencies = [ "addr", "affinitypool", @@ -7184,14 +7337,14 @@ dependencies = [ "pharos", "phf", "pin-project-lite", - "quick_cache", + "quick_cache 0.5.2", "radix_trie", "rand 0.8.5", "rayon", "reblessive", "regex", "reqwest", - "revision", + "revision 0.11.0", "ring", "rmpv", "roaring", @@ -7209,6 +7362,7 @@ dependencies = [ "storekey", "strsim", "subtle", + "surrealkv", "sysinfo", "tempfile", "thiserror 1.0.69", @@ -7220,7 +7374,7 @@ dependencies = [ "unicase", "url", "uuid", - "vart", + "vart 0.8.1", "wasm-bindgen-futures", "wasmtimer", "ws_stream_wasm", @@ -7228,8 +7382,8 @@ dependencies = [ [[package]] name = "surrealdb-migrations" -version = "2.2.2" -source = "git+https://github.com/disconsented/surrealdb-migrations.git#7f5b6bee020e77be3574159f12f780b2969d19d4" +version = "2.3.0" +source = "git+https://github.com/disconsented/surrealdb-migrations.git#e8c9a439310d163da6dc7f655661250d9e491f9f" dependencies = [ "chrono", "chrono-human-duration", @@ -7253,6 +7407,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "surrealkv" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43d55edab1e65c7704486016f98e9eac61c97474921dbac094af2cd16e16c3" +dependencies = [ + "ahash 0.8.12", + "bytes", + "chrono", + "crc32fast", + "double-ended-peekable", + "getrandom 0.2.16", + "lru", + "parking_lot", + "quick_cache 0.6.14", + "revision 0.10.0", + "vart 0.9.2", +] + [[package]] name = "syn" version = "1.0.109" @@ -7266,9 +7439,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -7292,7 +7465,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7445,7 +7618,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7456,17 +7629,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -7556,23 +7728,25 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenizers" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3169b3195f925496c895caee7978a335d49218488ef22375267fba5a46a40bd7" +checksum = "4c3846d8588abed0daba25a0e47edd58ea15e450a6088b2575f5116fdb0b27ca" dependencies = [ + "ahash 0.8.12", "aho-corasick", + "compact_str", + "dary_heap", "derive_builder", "esaxx-rs", - "getrandom 0.2.16", - "indicatif", - "itertools 0.13.0", - "lazy_static", + "getrandom 0.3.3", + "indicatif 0.17.11", + "itertools 0.14.0", "log", "macro_rules_attribute", "monostate", "onig", "paste", - "rand 0.8.5", + "rand 0.9.1", "rayon", "rayon-cond", "regex", @@ -7588,15 +7762,17 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -7610,7 +7786,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7651,14 +7827,14 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.26.2", + "tungstenite 0.27.0", ] [[package]] @@ -7677,44 +7853,75 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f271e09bde39ab52250160a67e88577e0559ad77e9085de6e9051a2c4353f8f8" +dependencies = [ + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "winnow", +] + [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" @@ -7735,9 +7942,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.1", "bytes", @@ -7776,13 +7983,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7798,9 +8005,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -7856,12 +8063,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "trim-in-place" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" - [[package]] name = "try-lock" version = "0.2.5" @@ -7891,9 +8092,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ "bytes", "log", @@ -8006,9 +8207,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -8022,6 +8223,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + [[package]] name = "universal-hash" version = "0.5.1" @@ -8129,6 +8336,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87782b74f898179396e93c0efabb38de0d58d50bbd47eae00c71b3a1144dbbae" +[[package]] +name = "vart" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03dccea250abfe68c00eee55f95af111e041b75bc11796cb83d1c05c5029efd9" + [[package]] name = "vcpkg" version = "0.2.15" @@ -8153,7 +8366,7 @@ checksum = "5b2d5567b6fbd34e8f0488d56b648e67c0d999535f4af2060d14f9074b43e833" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8183,9 +8396,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -8218,7 +8431,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -8253,7 +8466,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8315,9 +8528,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9c5f0bc545ea3b20b423e33b9b457764de0b3730cd957f6c6aa6c301785f6e" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" dependencies = [ "phf", "phf_codegen", @@ -8331,14 +8544,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" dependencies = [ "rustls-pki-types", ] @@ -8406,7 +8619,7 @@ dependencies = [ "windows-interface 0.59.1", "windows-link", "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-strings", ] [[package]] @@ -8417,7 +8630,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8428,7 +8641,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8439,7 +8652,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8450,24 +8663,24 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ + "windows-link", "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-strings", ] [[package]] @@ -8488,15 +8701,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -8533,6 +8737,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -8566,9 +8779,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -8720,9 +8933,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] @@ -8745,13 +8958,16 @@ dependencies = [ "chrono", "config", "humantime", - "indicatif", + "indicatif 0.18.0", "itertools 0.14.0", "lingua", + "log", + "macros", "reqwest", + "rs-snowflake", "salvo", "serde", - "serde-xml-rs 0.8.1", + "serde-xml-rs", "serde_json", "serde_repr", "snafu", @@ -8772,9 +8988,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "ws_stream_wasm" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" dependencies = [ "async_io_stream", "futures", @@ -8783,7 +8999,7 @@ dependencies = [ "pharos", "rustc_version", "send_wrapper", - "thiserror 1.0.69", + "thiserror 2.0.12", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -8800,9 +9016,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", "rustix", @@ -8810,9 +9026,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xxhash-rust" @@ -8822,9 +9038,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yaml-rust2" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18b783b2c2789414f8bb84ca3318fc9c2d7e7be1c22907d37839a58dedb369d3" +checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7" dependencies = [ "arraydeque", "encoding_rs", @@ -8869,7 +9085,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] @@ -8881,28 +9097,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8922,7 +9138,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] @@ -8962,7 +9178,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8975,7 +9191,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap 2.9.0", + "indexmap 2.10.0", "num_enum", "thiserror 1.0.69", ] @@ -8999,9 +9215,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.14" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index a881586..1a6fbc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] default-members = [] -members = ["classification"] +members = ["classification", "macros"] [features] @@ -14,14 +14,14 @@ license = "MPL-2" [dependencies] tokio = { version = "1.44", features = ["rt", "rt-multi-thread", "macros", "fs", "sync"] } -surrealdb = { version = "2.3", features = ["kv-rocksdb", "allocator", "protocol-http", "protocol-ws", "rustls"] } +surrealdb = { version = "2.3", features = ["kv-rocksdb", "allocator", "protocol-http", "protocol-ws", "rustls", "kv-mem"] } reqwest = { version = "0.12", features = ["json", ] } -indicatif = "0.17.11" +indicatif = "0.18" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_repr = "0.1" snafu = "0.8" -salvo = { version = "0.78", features = ["oapi", "logging", "serve-static", "affix-state", "cookie"] } +salvo = { version = "0.80", features = ["oapi", "logging", "serve-static", "affix-state", "cookie"] } config = "0.15" veil = "0.2" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } @@ -35,6 +35,9 @@ surrealdb-migrations = "2.2" serde-xml-rs = "0.8.1" chrono = "0.4" biscuit-auth = "6.0.0-beta.3" +rs-snowflake = "0.6" +macros = { path = "./macros" } +log = "0.4.27" [patch.crates-io] surrealdb-migrations = { git = "https://github.com/disconsented/surrealdb-migrations.git" } diff --git a/src/main.rs b/src/main.rs index af10f1a..cc525b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,8 @@ use crate::{ }; mod app_config; -mod auth; +pub(crate) mod auth; +mod db; mod language; mod model; mod steam; @@ -211,6 +212,7 @@ async fn insert_data(db: &Surreal, bb: &BBCode, data: Struct) -> Result<(), last_updated: data.time_updated.unwrap_or_default() as _, tags: vec![], score: data.vote_data.map(|votes| votes.score).unwrap_or_default(), + properties: vec![], }; let tags = data .tags diff --git a/src/model.rs b/src/model.rs index 5ea7674..25f5035 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,8 +1,11 @@ -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + ops::{Deref, DerefMut}, +}; use chrono::{DateTime, Utc}; use salvo::prelude::ToSchema; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; use serde_repr::{Deserialize_repr, Serialize_repr}; use surrealdb::{RecordId, RecordIdKey}; @@ -54,6 +57,7 @@ pub struct WorkshopItem { pub title: String, pub tags: Vec, pub score: f32, + pub properties: Vec>, } #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] pub struct FullWorkshopItem { @@ -70,6 +74,7 @@ pub struct FullWorkshopItem { pub title: String, // The titles name pub tags: Vec, // The list of tags found pub score: f32, // The "quality" score assigned by steam + pub properties: Vec>, // Approved or owned properties } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Dependencies { @@ -85,7 +90,7 @@ pub fn into_string(key: &RecordIdKey) -> String { } /// A steam workshop app -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] pub struct App { /// The steam ID for an app pub id: u32, @@ -105,10 +110,10 @@ pub struct App { } /// A workshop walker user -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct User { +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct User { /// The steam account ID - pub id: String, + pub id: T, /// Privileged access pub admin: bool, pub banned: bool, @@ -128,64 +133,116 @@ where } /// Crowdsourced metadata for an item, private version -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, Eq, PartialEq, Hash)] pub struct Property { - /// Snowflake generated ID - pub id: String, - /// Associated app ID, for enforcing uniqueness - pub app_id: u32, pub class: Class, pub value: String, +} +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct PropertyExt { /// Reasoning or justification for an inclusion pub note: Option, pub status: Status, - pub upvote_count: u64, + /// The current score + pub upvote_count: i64, + /// The total upvotes pub vote_count: u64, - /// The item that this is associated with - pub workshop_item: RecordId, + pub source: Source, +} +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct WorkshopItemProperties { + #[serde(rename = "in")] + pub workshop_item: CHILD, + #[serde(rename = "out")] + pub property: PROP, + #[serde(flatten)] + pub property_ext: PropertyExt, + pub vote_state: Option, } /// Crowdsourced relationships for an item, used for "soft" dependencies not /// supplied by steam, private version -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Companion { +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct Companion { /// Snowflake generated ID pub id: String, - pub r#in: RecordId, - pub out: RecordId, + pub r#in: R, + pub out: R, /// Reasoning or justification for an inclusion pub note: Option, pub status: Status, pub upvote_count: u64, pub vote_count: u64, - /// The item that this is associated with - pub workshop_item: RecordId, + pub source: Source, } /// A voting record -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] pub struct Vote { /// The app this is associated with, for possible filtering pub app_id: String, - pub link: RecordId, pub score: f32, - pub user: RecordId, pub when: DateTime, } -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(untagged)] -pub enum Source { +#[derive(Clone, Debug, ToSchema, Eq, PartialEq)] +pub enum Source { /// Auto-generated System, /// User submitted - User(RecordId), + User(T), +} + +impl serde::Serialize for Source +where + T: serde::Serialize, +{ + fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error> + where + __S: Serializer, + { + match *self { + Source::System => __serializer.serialize_str("system"), + Source::User(ref __field0) => (*__field0).serialize(__serializer), + } + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for Source { + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let __content = + ::deserialize(deserializer)?; + let __deserializer = + serde::__private::de::ContentRefDeserializer::::new(&__content); + + if let Ok(__ok) = match String::deserialize(__deserializer) { + Ok(str) if str == "system" => Ok(Source::System), + + Err(__err) => Err(__err), + Ok(variant) => Err(D::Error::unknown_variant(&variant, &["system"])), + } { + return Ok(__ok); + } + if let Ok(__ok) = Result::map( + ::deserialize(__deserializer), + Source::User, + ) { + return Ok(__ok); + } + + Err(D::Error::custom( + "Expected either T or 'system' got neither", + )) + } } -#[derive(Debug, ToSchema, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Debug, ToSchema, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Class { /// Anything like addon, overhaul, bugfix, patch - r#Type, + Type, /// Literary themes like mecha Theme, /// Literary genres like `CyberPunk` @@ -214,3 +271,141 @@ pub enum Status { Pending = 0, Accepted = 1, } + +#[derive(Serialize, Deserialize, PartialOrd, PartialEq, Eq, Debug)] +#[serde(transparent)] +struct Id(RecordId); + +impl From for Id { + fn from(value: RecordId) -> Self { + Self(value) + } +} +impl From for RecordId { + fn from(val: Id) -> Self { + val.0 + } +} + +impl Deref for Id { + type Target = RecordId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Id { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl salvo::oapi::ToSchema for Id { + fn to_schema( + components: &mut salvo::oapi::Components, + ) -> salvo::oapi::RefOr { + let name = salvo::oapi::naming::assign_name::(salvo::oapi::naming::NameRule::Auto); + let ref_or = salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!( + "#/components/schemas/{name}" + ))); + if !components.schemas.contains_key(&name) { + components.schemas.insert(name.clone(), ref_or.clone()); + let schema = salvo::oapi::Object::new().schema_type( + salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String), + ); + components.schemas.insert(name, schema); + } + ref_or + } +} + +#[cfg(test)] +mod test { + + use serde::{Deserialize, Serialize}; + use surrealdb::RecordId; + + use crate::model::{Class, Id, Source}; + + #[test] + fn test_id_newtype() { + let id: Id = RecordId::from_table_key("items", "1").into(); + let id_txt = serde_json::to_string(&id).unwrap(); + let id2: Id = serde_json::from_str(&id_txt).unwrap(); + assert_eq!(id, id2); + + println!("{id_txt}"); + } + + #[test] + fn test_source_de_ser() { + let system: Source = Source::System; + let system_text = serde_json::to_string(&system).unwrap(); + let system2 = serde_json::from_str(&system_text).unwrap(); + assert_eq!(system, system2); + let user = Source::User("a".to_string()); + let user_text = serde_json::to_string(&user).unwrap(); + let user2 = serde_json::from_str(&user_text).unwrap(); + assert_eq!(user, user2); + println!("{user_text} {system_text}"); + + { + let user = Source::User(RecordId::from_table_key("a", "b")); + let user_text = serde_json::to_string(&user).unwrap(); + let user2 = serde_json::from_str(&user_text).unwrap(); + assert_eq!(user, user2); + println!("{user_text} {system_text}"); + } + + #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] + struct Test { + source: Source, + } + + let t_user = Test { source: user }; + let txt_user = serde_json::to_string(&t_user).unwrap(); + assert_eq!(t_user, serde_json::from_str(&txt_user).unwrap()); + let t_sys = Test { source: system }; + let txt_sys = serde_json::to_string(&t_sys).unwrap(); + assert_eq!(t_sys, serde_json::from_str(&txt_sys).unwrap()); + println!("{txt_user} {txt_sys}"); + #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] + struct Foo { + thing: Class, + } + println!( + "{}", + serde_json::to_string(&Foo { + thing: Class::Genre + }) + .unwrap() + ); + } + #[tokio::test] + async fn test_source_surreal() { + use surrealdb::{Surreal, engine::local::Mem}; + + #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] + struct Foo { + source: Source, + } + let db = Surreal::new::(()).await.unwrap(); + db.use_ns("test").use_db("test").await.unwrap(); + db.query("DEFINE TABLE OVERWRITE properties TYPE NORMAL SCHEMAFULL PERMISSIONS NONE;") + .query( + "DEFINE FIELD OVERWRITE source ON properties TYPE 'system' | record \ + PERMISSIONS FULL;", + ) + .await + .unwrap(); + let foo_struct = Foo { + source: Source::System, + }; + let mut r: Vec = db + .insert("properties") + .content(foo_struct.clone()) + .await + .unwrap(); + assert_eq!(foo_struct, r.pop().unwrap()); + } +} diff --git a/src/web/admin.rs b/src/web/admin.rs new file mode 100644 index 0000000..74ef6b6 --- /dev/null +++ b/src/web/admin.rs @@ -0,0 +1,134 @@ +use log::error; +use reqwest::StatusCode; +use salvo::{ + Depot, Response, Writer, handler, + oapi::{ToSchema, extract::JsonBody}, + prelude::{Json, endpoint}, +}; +use serde::{Deserialize, Serialize}; +use surrealdb::{Surreal, engine::local::Db}; + +use crate::{ + db::{ItemID, UserID}, + model::{Property, Status, User, WorkshopItemProperties}, +}; + +#[endpoint] +pub async fn get_users(depot: &mut Depot, response: &mut Response) { + match depot + .obtain::>() + .expect("getting shared state") + .query("SELECT record::id(id) as id, * FROM users") + .await + .map(|mut q| q.take(0)) + { + Ok(Ok(results)) => { + response.render(Json::>>(results)); + } + Ok(Err(e)) | Err(e) => { + error!("{e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + } + } +} + +#[endpoint] +pub async fn patch_user(data: JsonBody, depot: &mut Depot, response: &mut Response) { + let id = UserID::from(data.0.id); + if let Some(banned) = data.0.banned { + let res = depot + .obtain::>() + .expect("getting shared state") + .query("UPDATE $user SET banned=$banned") + .bind(("user", id.clone())) + .bind(("banned", banned)) + .await; + if let Err(e) = res { + error!("{e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + return; + } + } + + if let Some(admin) = data.0.admin { + let res = depot + .obtain::>() + .expect("getting shared state") + .query("UPDATE $user SET $admin=admin") + .bind(("user", id)) + .bind(("admin", admin)) + .await; + if let Err(e) = res { + error!("{e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + return; + } + } + response.status_code(StatusCode::NO_CONTENT); +} + +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct PatchUser { + pub id: String, + pub banned: Option, + pub admin: Option, +} + +#[endpoint] +pub async fn get_workshop_item_properties(depot: &mut Depot, response: &mut Response) { + match depot + .obtain::>() + .expect("getting shared state") + .query( + "SELECT record::id(in) as in, out.*.id.{class,value} as out, source.to_string(), \ + id.to_string(), * FROM workshop_item_properties", + ) + .await + .map(|mut q| q.take(0)) + { + Ok(Ok(results)) => { + response.render(Json::>>( + results, + )); + } + Ok(Err(e)) | Err(e) => { + error!("{e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + } + } +} + +#[handler] +pub async fn patch_workshop_item_properties( + data: JsonBody, + depot: &mut Depot, + response: &mut Response, +) { + let res = depot + .obtain::>() + .expect("getting shared state") + .query("LET $link = properties:{class: $class, value: $value}") + .query( + "UPDATE ONLY workshop_item_properties SET status=$status WHERE in = $item AND out = \ + $link;", + ) + .bind(("class", data.0.property.class)) + .bind(("value", data.0.property.value)) + .bind(("item", ItemID::from(data.0.item).into_recordid())) + .bind(("status", data.0.status)) + .await; + if let Err(e) = res { + error!("{e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + return; + } + response.status_code(StatusCode::NO_CONTENT); +} + +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct PatchRelationship { + pub item: String, + #[serde(flatten)] + pub property: Property, + pub status: Status, +} diff --git a/src/web/mod.rs b/src/web/mod.rs index 1605654..a535438 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,4 +1,11 @@ -use std::{str::FromStr, sync::Arc}; +mod admin; +mod companions; +mod properties; + +use std::{ + str::FromStr, + sync::{Arc, OnceLock}, +}; use itertools::Itertools; use salvo::{ @@ -12,6 +19,7 @@ use salvo::{ }; use serde_json::Map; use snafu::{OptionExt, ResultExt, Whatever}; +use snowflake::SnowflakeIdGenerator; use str_macro::str; use surrealdb::{ RecordId, Surreal, @@ -20,32 +28,76 @@ use surrealdb::{ sql::{Cond, Expression, Field, Operator, statements::SelectStatement, to_value}, syn::idiom, }; -use tokio::sync::OnceCell; +use tokio::sync::{Mutex, OnceCell}; use tracing::{Instrument, info_span, instrument}; use crate::{ app_config::Config, auth, + db::UserID, language::DetectedLanguage, model::{FullWorkshopItem, OrderBy, WorkshopItem, into_string}, }; /// Global static DB_POOL: OnceCell> = OnceCell::const_new(); +static ID_GENERATOR: OnceLock>> = OnceLock::new(); + +fn get_gen() -> Arc> { + Arc::clone(ID_GENERATOR.get_or_init(|| Arc::new(Mutex::new(SnowflakeIdGenerator::new(1, 1))))) +} /// Start the webserver returning once it exists pub async fn start(db: Surreal, config: Arc) { let db = DB_POOL.get_or_init(|| async { db }).await.clone(); - let router = Router::new() - .push(Router::with_path("api/list").get(list)) - .push(Router::with_path("api/item/{id}").get(get)) - .push( - Router::with_path("api") - .hoop(affix_state::inject(config)) - .hoop(affix_state::inject(db)) - .push(Router::with_path("login").get(auth::redirect)) - .push(Router::with_path("verify").get(auth::verify)) - .push(Router::with_path("logout").get(auth::invalidate)), - ); + let router = Router::new().push( + Router::with_path("api") + .push(Router::with_path("list").get(list)) + .push( + Router::with_path("item/{id}") + .hoop(affix_state::inject(config.clone())) + .hoop(auth::validate_opt) + .get(get), + ) + .push( + Router::with_path("property") + .hoop(affix_state::inject(config.clone())) + .hoop(auth::validate) + .post(properties::new), + ) + .push( + Router::with_path("vote") + .hoop(affix_state::inject(config.clone())) + .hoop(auth::validate) + .hoop(affix_state::inject(db.clone())) + .push( + Router::with_path("property") + .post(properties::vote) + .delete(properties::remove), + ), + ) + .push( + Router::with_path("admin") + .hoop(affix_state::inject(config.clone())) + .hoop(auth::validate) + .hoop(affix_state::inject(db.clone())) + .hoop(auth::enforce_admin) + .push( + Router::with_path("properties") + .put(admin::patch_workshop_item_properties) + .get(admin::get_workshop_item_properties), + ) + .push( + Router::with_path("users") + .get(admin::get_users) + .put(admin::patch_user), + ), + ) + .hoop(affix_state::inject(config)) + .hoop(affix_state::inject(db)) + .push(Router::with_path("login").get(auth::redirect)) + .push(Router::with_path("verify").get(auth::verify)) + .push(Router::with_path("logout").get(auth::invalidate)), + ); let doc = OpenApi::new("workshop-walker", "0.0.1").merge_router(&router); let router = router .push(doc.into_router("/api-doc/openapi.json")) @@ -100,24 +152,30 @@ impl Writer for Error { #[endpoint] #[instrument] -async fn get(id: PathParam) -> Result> { +async fn get(id: PathParam, depot: &mut Depot) -> Result> { let id = id.0; let db: &Surreal = DB_POOL.get().expect("DB not initialised"); - async fn query(id: String, db: &Surreal) -> Result { + let user = auth::get_user(depot).await; + async fn query( + id: String, + db: &Surreal, + user: Option, + ) -> Result { let id = RecordId::from_table_key("workshop_items", &id); let mut response = db .query( "SELECT in.appid as appid, in.description as description, in.id as id, in.title as title, in.author as author, in.languages as languages, in.last_updated as last_updated, in.score as score, in.tags.{id: id.to_string(), app_id, \ - display_name} as tags, in.preview_url as preview_url FROM \ + display_name} as tags, in.preview_url as preview_url, [] as properties FROM \ $id<-item_dependencies.*;", ) .query( "SELECT out.appid as appid, out.description as description, out.id as id, out.author as author, out.languages as languages, out.last_updated as last_updated, out.title as title, out.score as score, out.tags.{id: \ - id.to_string(), app_id, display_name} as tags, out.preview_url as preview_url + id.to_string(), app_id, display_name} as tags, out.preview_url as preview_url, \ + [] as properties FROM $id->item_dependencies.*;", ) .bind(("id", id.clone())) @@ -129,13 +187,53 @@ async fn get(id: PathParam) -> Result> { response.take(1).whatever_context("no dependencies found")?; let result = { - let mut res = db - .query( - r"SELECT *, tags.{id: id.to_string(), app_id, display_name} as tags FROM $id", - ) - .bind(("id", id)) - .await - .whatever_context("getting item")?; + let mut res = match user { + None => db + .query( + r"SELECT *, tags.{id: id.to_string(), app_id, display_name} as tags, + ->workshop_item_properties.filter(|$prop|$prop.status == 1)[*].{ + id: id.to_string(), + in: in.to_string(), + out: out.id.{ + class, + `value` + }, + source: 'system', + status, + upvote_count, + vote_count + } as properties FROM $id", + ) + .bind(("id", id)) + .await + .whatever_context("getting item (unauth)")?, + // array::filter doesn't capture scoped variables yet hence why this is being + // formatted in + Some(user) => db + .query(format!( + "SELECT *, tags.{{id: id.to_string(), app_id, display_name}} as tags, + ->workshop_item_properties.filter(|$prop|$prop.status == 1 \ + || $prop.source == {})[*].{{ + id: id.to_string(), + in: in.to_string(), + out: out.id.{{ + class, + `value` + }}, + source: 'system', + status, + upvote_count, + vote_count, + vote_state: votes:{{item: $id, link: out, user: \ + {0}}}.score + }} as properties FROM $id", + UserID::from(user).into_recordid() + )) + .bind(("id", id)) + .await + .whatever_context("getting item (auth)")?, + }; + let result: Option> = res.take(0).whatever_context("not found")?; result.whatever_context("not found")? @@ -165,6 +263,7 @@ async fn get(id: PathParam) -> Result> { last_updated: e.last_updated, tags: e.tags, score: e.score, + properties: e.properties, }) .collect(), @@ -183,13 +282,15 @@ async fn get(id: PathParam) -> Result> { last_updated: e.last_updated, tags: e.tags, score: e.score, + properties: e.properties, }) .collect(), tags: result.tags, score: result.score, + properties: result.properties, }) } - let results = query(id, db).await?; + let results = query(id, db, user).await?; Ok(Json(results)) } @@ -231,6 +332,28 @@ async fn list( alias: Some("tags".into()), }); } + { + stmt.expr.0.push(Field::Single { + // Select _approved_ props only + expr: idiom( + r"->workshop_item_properties.filter(|$prop|$prop.status == 1)[*].{ + id: id.to_string(), + in: in.to_string(), + out: out.id.{ + class, + `value` + }, + source: 'system', + status, + upvote_count, + vote_count + }", + ) + .expect("expanding properties idiom") + .into(), + alias: Some("properties".into()), + }); + } if let Some(OrderBy::Dependents) = order_by { stmt.expr.0.push(Field::Single { expr: idiom(" <-item_dependencies.len()") @@ -413,6 +536,7 @@ async fn list( last_updated: res.last_updated, tags: res.tags, score: res.score, + properties: res.properties, }) .collect()) } diff --git a/src/web/properties.rs b/src/web/properties.rs new file mode 100644 index 0000000..5d875fc --- /dev/null +++ b/src/web/properties.rs @@ -0,0 +1,310 @@ +use biscuit_auth::Authorizer; +use chrono::{DateTime, Utc}; +use log::{debug, error}; +use salvo::{ + Depot, Response, Writer, + fuse::flex::Guard, + oapi::extract::JsonBody, + prelude::{StatusCode, ToSchema, endpoint}, +}; +use serde::{Deserialize, Serialize}; +use surrealdb::{RecordId, Surreal, engine::local::Db}; + +use crate::{ + db::{ItemID, UserID}, + model::{Class, Property, Source, WorkshopItemProperties}, + web::DB_POOL, +}; + +/// Add or change a vote for a property. +/// Prop must be accepted otherwise, fail. +/// Must be either an upvote (1) or a downvote (0), not used for removal. +#[endpoint] +pub async fn vote(vote_data: JsonBody, depot: &mut Depot, response: &mut Response) { + if vote_data.score != 1 && vote_data.score != -1 { + response.status_code(StatusCode::BAD_REQUEST); + return; + } + let db: &Surreal = DB_POOL.get().expect("DB not initialised"); + let user = crate::auth::get_user(depot) + .await + .expect("already authenticated"); + let user = UserID::from(user); + &user; + &vote_data; + let query = db + // .query("BEGIN TRANSACTION;") + .query("LET $link = properties:{class: $class, value: $value}") + .query(r#"IF !record::exists($link){THROW "FAIL LINK";}"#) + .query(r#"IF !record::exists($item){THROW "FAIL ITEM";}"#) + .query( + "LET $changed = INSERT INTO votes (id, score, when) VALUES ({link: $link, user: \ + $user, item: $item}, $score, time::now()) ON DUPLICATE KEY UPDATE when=time::now(), \ + score=$score RETURN BEFORE;", + ) + // only increment the vote count on an insertion, as score is truthy + .query( + r" + LET $changed_score = $changed.score[0]; + IF $changed_score && $changed_score != $score{ + LET $vote_diff = ($changed_score * -1); + UPDATE ONLY workshop_item_properties SET vote_count += $vote_count, upvote_count += $vote_diff WHERE in = $item AND out = $link; + RETURN $vote_diff + } else if !$changed_score{ + UPDATE ONLY workshop_item_properties SET vote_count += 1, upvote_count += $score WHERE in = $item AND out = $link; + } else { + return {chnaged_score: $changed_score, score: $score}; + };", + ) + // .query("COMMIT TRANSACTION;") + .bind(("class", vote_data.0.class)) + .bind(("value", vote_data.0.value)) + .bind(("user", user.into_recordid())) + .bind(( + "item", + RecordId::from_table_key("workshop_items", vote_data.0.item), + )) + .bind(("score", vote_data.0.score)); + let result = query.await; + match result { + Ok(e) => match e.check() { + Ok(v) => { + response.status_code(StatusCode::NO_CONTENT); + } + Err(err) => { + debug!("bad vote from user: {err}"); + response.status_code(StatusCode::BAD_REQUEST); + } + }, + Err(e) => { + error!("vote query error: {e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + } + } +} +#[endpoint] +pub async fn remove(vote_data: JsonBody, depot: &mut Depot, response: &mut Response) { + let db: &Surreal = DB_POOL.get().expect("DB not initialised"); + let user = crate::auth::get_user(depot) + .await + .expect("already authenticated"); + let user = UserID::from(user); + let result = db + .query("BEGIN TRANSACTION;") + .query("LET $link = properties:{class: $class, value: $value}") + .query( + "let $before = DELETE only votes:{link: $link, user: $user, item: $item} RETURN \ + BEFORE;", + ) + .query( + // Update if there _was_ an entry deleted + "if $before.score{RETURN UPDATE ONLY workshop_item_properties SET \ + vote_count=math::max([vote_count-1, 0]), upvote_count-=$before.score WHERE in=$item \ + AND out=$link RETURN diff};", + ) + .query("COMMIT TRANSACTION;") + .bind(("class", vote_data.0.class)) + .bind(("value", vote_data.0.value)) + .bind(("user", user.into_recordid())) + .bind(( + "item", + RecordId::from_table_key("workshop_items", vote_data.0.item), + )) + .await; + match result { + Ok(e) => match e.check() { + Ok(v) => { + response.status_code(StatusCode::NO_CONTENT); + } + Err(err) => { + debug!("bad vote from user: {err:?}"); + response.status_code(StatusCode::BAD_REQUEST); + } + }, + Err(e) => { + error!("vote query error: {e:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + } + } +} +/// Add a new property with the following rules: +/// Must be entirely new or an exact match to an existing property. +/// Likeness checks are done on the value only (for now) using +/// damerau_levenshtein distance. +#[endpoint] +pub async fn new(new_property: JsonBody, depot: &mut Depot, response: &mut Response) { + let workshop_id = ItemID::from(new_property.0.workshop_item).into_recordid(); + // Select similar properties + let db: &Surreal = DB_POOL.get().expect("DB not initialised"); + let test_prop = Property { + class: new_property.0.class, + value: new_property.0.value, + }; + let prop_exists = { + #[derive(Serialize, Deserialize, Clone, Debug)] + struct Temp { + id: surrealdb::Value, + } + let query = db + .query( + "SELECT id.class as class, id.value as value FROM properties WHERE \ + string::distance::damerau_levenshtein(id.value, $value) < 0.8;", + ) + .query( + "SELECT *, in.to_string(), out.*.id.{class,value} as out, source.to_string() OMIT \ + id FROM workshop_item_properties WHERE in=$workshop_item", + ) + .bind(("workshop_item", workshop_id.clone())) + .bind(("value", test_prop.value.clone())); + let mut res = query.await.unwrap(); + res = res.check().unwrap(); + + let similar_properties = res.take::>(0).unwrap(); + if !similar_properties.is_empty() && !similar_properties.contains(&test_prop) { + // return similar entries + error!("Similar props exist: {similar_properties:?}"); + response.status_code(StatusCode::CONFLICT); + response.body(format!("{similar_properties:?}")); + return; + } + debug!("Succeeded similar_properties test"); + let existing_properties: Vec> = + res.take(1).unwrap(); + existing_properties + .iter() + .any(|prop| prop.property == test_prop) + }; + debug!("{test_prop:?} already exists? {prop_exists}"); + let authorizer = depot + .obtain_mut::() + .expect("getting shared state"); + let (userid, _): (String, i64) = authorizer + .query_exactly_one("data($user, 0) <- user($user)") + .unwrap(); + + // Insert any new properties + + { + match db + .query( + "INSERT IGNORE INTO properties (id) values (properties:{class: $class, value: \ + $value});", + ) + .bind(("class", test_prop.class)) + .bind(("value", test_prop.value)) + .query( + "RELATE $workshop_id->workshop_item_properties->properties:{class: $class, \ + value:$value} SET note=$note, source=$source;", + ) + .bind(("workshop_id", workshop_id)) + .bind(("note", new_property.0.note)) + .bind(( + "source", + Source::::User(UserID::from(userid).into()), + )) + .await + .unwrap() + .check() + { + Ok(_) => { + response.status_code(StatusCode::NO_CONTENT); + } + Err(surrealdb::Error::Db(surrealdb::err::Error::IndexExists { .. })) => { + // Already exists, may be pending or rejected + response.status_code(StatusCode::CONFLICT); + response.body("Property already exists; Maybe pending or denied"); + } + Err(other) => { + error!("{other:?}"); + response.status_code(StatusCode::INTERNAL_SERVER_ERROR); + } + } + } +} + +/// Crowdsourced metadata for an item, private version +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct NewProperty { + pub class: Class, + pub value: String, + /// Reasoning or justification for an inclusion + pub note: Option, + pub workshop_item: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)] +pub struct VoteData { + pub item: String, + pub class: Class, + pub value: String, + // Must be -1 or 1 for now + pub score: i8, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct UpdateVote { + pub user: UserID, + pub when: DateTime, +} + +#[cfg(test)] +mod test { + + use surrealdb::{Surreal, engine::local::Mem}; + + use crate::model::{Class, Property}; + + #[test] + fn test_biscuit_conversion() { + use biscuit_auth::{Biscuit, KeyPair}; + let keypair = KeyPair::new(); + let builder = Biscuit::builder().fact("user(\"John Doe\", 42)").unwrap(); + + let biscuit = builder.build(&keypair).unwrap(); + + let mut authorizer = biscuit.authorizer().unwrap(); + // Biscuit doesn't have support for querying for a single string, so, we do this + // instead. + let res: (String, i64) = authorizer + .query_exactly_one("data($name, 0) <- user($name, $id)") + .unwrap(); + assert_eq!(res.0, "John Doe"); + assert_eq!(res.1, 0); + } + + #[tokio::test] + async fn test_prop_db() { + let db = Surreal::new::(()).await.unwrap(); + db.use_ns("test").use_db("test").await.unwrap(); + db.query( + "DEFINE TABLE OVERWRITE properties TYPE NORMAL SCHEMAFULL PERMISSIONS NONE; DEFINE \ + FIELD id ON properties TYPE { class: string, value: string } PERMISSIONS FULL;", + ) + .await + .unwrap(); + let _: Vec = db + .insert("properties") + .content(Property { + class: Class::Type, + value: "test".to_string(), + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_throw() { + let db = Surreal::new::(()).await.unwrap(); + db.use_ns("test").use_db("test").await.unwrap(); + let output = db + .query("DEFINE TABLE OVERWRITE properties TYPE NORMAL SCHEMAFULL PERMISSIONS NONE;") + .query("CREATE properties:ShouldExist;") + .query("BEGIN TRANSACTION;") + .query("CREATE properties:NotExist;") + .query("IF true{THROW \"GRACEFUL\"};") + .query("SELECT * FROM properties;") + .await + .unwrap(); + dbg!(output.check().unwrap()); + } +} diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 0b435eb..9b5390a 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -28,22 +28,21 @@ Workshop Walker {#snippet trail()} {#if logged_in} - + Sign Out {:else} Sign In Through Steam + src="https://steamcdn-a.akamaihd.net/steamcommunity/public/images/steamworks_docs/english/sits_small.png" + /> {/if} - {/snippet} diff --git a/ui/src/routes/admin/+page.svelte b/ui/src/routes/admin/+page.svelte new file mode 100644 index 0000000..acca313 --- /dev/null +++ b/ui/src/routes/admin/+page.svelte @@ -0,0 +1,212 @@ + + +
+

Property Management System

+ + (group = e.value)}> + {#snippet list()} + Properties + Users + {/snippet} + {#snippet content()} + + +
+ + +
+ + + + + + + + + + + + + + {#each properties as property} + {@debug property} + + + + + + + + + {/each} + +
IDClassValueSubmitted ByStatusActions
{property.in}{property.out.class}{property.out.value}{property.source} + {#if property.status === -1} + Denied + {:else if property.status === 0} + Pending + {:else} + Approved + {/if} + + +
+
+ + + + + + + + + + + + + + + {#each users as user} + {@debug user} + + + + + + + + {/each} + +
IDAdminBannedActionsLast Logged In
{user.id} + toggleUserAdmin(user.id, e.target.checked)} + /> + + {toggleUserBan(user.id, e.target.checked)}} + /> + + {user.last_logged_in} + + +
+
+ {/snippet} +
+
diff --git a/ui/src/routes/admin/+page.ts b/ui/src/routes/admin/+page.ts new file mode 100644 index 0000000..119c7d1 --- /dev/null +++ b/ui/src/routes/admin/+page.ts @@ -0,0 +1,16 @@ + +export const prerender = false; +export const load: PageLoad = async ({ fetch, params }) => { + return { + users: fetch(`/api/admin/users`).then((res) => { + if (res.ok) { + return res.json(); + } + }), + properties: fetch(`/api/admin/properties`).then((res) => { + if (res.ok) { + return res.json(); + } + }) + }; +}; diff --git a/ui/src/routes/app/[id]/+page.svelte b/ui/src/routes/app/[id]/+page.svelte index dac8ec2..3dd0113 100644 --- a/ui/src/routes/app/[id]/+page.svelte +++ b/ui/src/routes/app/[id]/+page.svelte @@ -20,6 +20,7 @@ import TimePicker from '$lib/timePicker.svelte'; import { Shadow } from 'svelte-loading-spinners'; import { invalidate } from '$app/navigation'; + import Property from '../../item/[item]/Property.svelte'; import { Switch } from '@skeletonlabs/skeleton-svelte'; let { data }: { data: PageData } = $props(); @@ -39,6 +40,8 @@ return url.pathname === '/api/list'; }); } + + const logged_in = document.cookie.includes('token_set='); @@ -333,12 +336,28 @@

{item.description}

-
- {#each item.tags as tag (tag.id)} - {tag.display_name} - {:else} - - - {/each} +
+
+ {#each item.tags as tag (tag.id)} + {tag.display_name} + {:else} + - + {/each} +
+ {#if item.properties && item.properties.length > 0} +
+
+ {#each item.properties as prop (prop.id)} + {@debug prop} + + {/each} +
+ {/if}
{:else} diff --git a/ui/src/routes/item/[item]/+page.svelte b/ui/src/routes/item/[item]/+page.svelte index 4ebeac4..e3a57c6 100644 --- a/ui/src/routes/item/[item]/+page.svelte +++ b/ui/src/routes/item/[item]/+page.svelte @@ -6,10 +6,14 @@ fa1, faArrowLeft, faArrowRight, + faChevronDown, + faChevronUp, faCross, faEllipsis, faFilter, faLink, + faThumbsDown, + faThumbsUp, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; import { Switch } from '@skeletonlabs/skeleton-svelte'; @@ -17,6 +21,10 @@ import { Pagination } from '@skeletonlabs/skeleton-svelte'; import TimeAgo from '$lib/timeAgo.svelte'; import { Shadow } from 'svelte-loading-spinners'; + import PropertyPrompt from './PropertyPrompt.svelte'; + import Property from './Property.svelte'; + import { Modal } from '@skeletonlabs/skeleton-svelte'; + import { onNavigate } from '$app/navigation'; let { data }: { data: PageData } = $props(); console.log('Hello, wolrd!', data); @@ -93,6 +101,17 @@ let size = $state(20); const slicedSource = $derived((s) => s.slice((page - 1) * size, page * size)); $inspect(slicedSource); + let openPanels = $state(['relations', 'companions', 'description']); + + let loginModalState = $state(false); + + let location = $state(encodeURI(document.location.pathname)); + onNavigate((navigation) => { + console.log(navigation); + location = encodeURI(navigation.to.url.pathname); + }); + + const logged_in = document.cookie.includes('token_set='); @@ -105,6 +124,7 @@ {/await} +{@render loginModal()} {#await data}
@@ -123,9 +143,29 @@
{@render titleCard()} - {@render relations()} - - {@render description()} + (openPanels = e.value)} + multiple + padding="" + > + + + {#snippet lead()} + + {/snippet} + {#snippet control()}Relations{/snippet} + + {#snippet panel()}{@render relations()}{/snippet} + + + {#snippet lead()} + + {/snippet} + {#snippet control()}Description{/snippet} + {#snippet panel()}{@render description()}{/snippet} + +
@@ -135,7 +175,7 @@ {#snippet linkSet(item)}
@@ -266,6 +306,46 @@ {/each}
+ +
+ + Properties: +
+ {#each item.properties as property} + {@debug property} + + {:else} + No Properties; Submit one? + {#if !logged_in} + { + loginModalState = true; + }} + item={item.id} + > + {/if} + {/each} +
+
+
+ {#if logged_in} + +
+ { + loginModalState = true; + }} + item={item.id} + > +
+ {/if} {/snippet} @@ -305,7 +385,6 @@
-

Relations

Dependencies

{#if item.dependencies.length > 0} -
+
{#each item.dependencies as dependency (dependency.id)} {@render linkSet(dependency)} {/each} @@ -431,8 +510,7 @@ {/if} Dependents {#if filteredDependents.length > 0} -
- +
{#each slicedSource(filteredDependents) as dependent (dependent.id)} {@render linkSet(dependent)} {/each} @@ -483,3 +561,292 @@
{/snippet} + +{#snippet companions()} +
+

Relations

+ + (filterPanel = e.value)} + collapsible + classes="col-span-4" + > + + + {#snippet lead()}Filter{/snippet} + {#snippet control()} + + {/snippet} + + {#snippet panel()} +
+
+ Tags +
+ {#each tags as tag} + + {/each} +
+
+
+ Languages +
+ {#each languages as lang} + + {/each} +
+
+
+ +
+
+ {/snippet} +
+
+
+ +
+

Dependencies

+ {#if item.dependencies.length > 0} +
+ {#each item.dependencies as dependency (dependency.id)} + {@render companionCard(dependency)} + {/each} +
+ {:else} +

No dependencies

+ {/if} +
+ +
+ + + (page = e.page)} + pageSize={size} + onPageSizeChange={(e) => (size = e.pageSize)} + siblingCount={4} + > + {#snippet labelEllipsis()} + + {/snippet} + {#snippet labelNext()} + + {/snippet} + {#snippet labelPrevious()} + + {/snippet} + {#snippet labelFirst()} + + {/snippet} + {#snippet labelLast()} + + {/snippet} + +
+ +
+

+ {#if filteredDependents.length > 0}{filteredDependents.length} + {/if} Dependents +

+ {#if filteredDependents.length > 0} +
+ + {#each slicedSource(filteredDependents) as dependent (dependent.id)} + {@render companionCard(dependent)} + {/each} +
+ {:else} +

No dependents

+ {/if} +
+ +
+ + + (page = e.page)} + pageSize={size} + onPageSizeChange={(e) => (size = e.pageSize)} + siblingCount={4} + > + {#snippet labelEllipsis()} + + {/snippet} + {#snippet labelNext()} + + {/snippet} + {#snippet labelPrevious()} + + {/snippet} + {#snippet labelFirst()} + + {/snippet} + {#snippet labelLast()} + + {/snippet} + +
+
+{/snippet} + +{#snippet companionCard(item)} +
+ +
+ + banner +
+ {item.title} +
+
+ + +
+ + + {item.votes ?? 0} + + +
+
+ + +
+ {#each item.tags as tag (tag.id)} + + {tag.display_name} + + {:else} + No tags + {/each} +
+ + + +
+{/snippet} + +{#snippet loginModal()} + (loginModalState = e.open)} + triggerBase="btn preset-tonal" + contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-screen-sm" + backdropClasses="backdrop-blur-sm" + > + {#snippet content()} +
+

Please Login To Continue

+
+
+ + Sign In Through Steam + + +
+ {/snippet} +
+{/snippet} diff --git a/ui/src/routes/item/[item]/Property.svelte b/ui/src/routes/item/[item]/Property.svelte new file mode 100644 index 0000000..93203f9 --- /dev/null +++ b/ui/src/routes/item/[item]/Property.svelte @@ -0,0 +1,112 @@ + + +
+ {#if property.status === -1} + + Rejected + {:else if property.status === 0} + + Pending + {/if} + {property.class}: + {property.value} + + {#if property.status === 1 && !hideVote} + +
+ + {property.upvote_count ?? 0} + +
+ {/if} +
diff --git a/ui/src/routes/item/[item]/PropertyPrompt.svelte b/ui/src/routes/item/[item]/PropertyPrompt.svelte new file mode 100644 index 0000000..d6223d1 --- /dev/null +++ b/ui/src/routes/item/[item]/PropertyPrompt.svelte @@ -0,0 +1,138 @@ + + +{#if loggedIn} + {#if request} + {#await request} +
+
+ +
+
+ +
+
+ +
+
+ {:then value} + {@debug value} + {#if !value.ok} +
+ +
+ {value.statusText} + {#await value.text() then body}- {body}{/await} +
+ +
+ {:else if true} + +
+ +
Submitted: Pending Approval
+ +
+ {:else} + +
+ +
Rejected: {'reason'}
+ +
+ {/if} + {:catch error} + +

Something went wrong: {error.message}

+ {/await} + {:else} +
{ + e.preventDefault(); + submitNew(); + }} + > +
+ +
+ + +
+ +
+
+ {/if} +{:else} +
+ + + +
+{/if} From 5ddbe3fd3f720c217b5843a87e2270ae68562127 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:32:33 +1200 Subject: [PATCH 13/19] build(Docker): Add macro's directory --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 0fe554f..022cc18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && apt-get install -y npm libclang-dev WORKDIR /usr/src/workshop-walker COPY src/ /usr/src/workshop-walker/src/ COPY classification/ /usr/src/workshop-walker/classification/ +COPY macros/ /usr/src/workshop-walker/macros/ COPY Cargo.lock Cargo.toml /usr/src/workshop-walker/ RUN cargo build --release --all From 5847d79831bc195fda1c123605da77b338a4c55b Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:32:43 +1200 Subject: [PATCH 14/19] chore: Misc tasks and tidying --- classification/prompt1.txt | 3 ++- src/language.rs | 5 ++--- ui/project.inlang/settings.json | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/classification/prompt1.txt b/classification/prompt1.txt index 1b12372..2a6e540 100644 --- a/classification/prompt1.txt +++ b/classification/prompt1.txt @@ -3,7 +3,8 @@ Extract the following information from the Steam Workshop item in strict JSON format (all lowercase): - "types": Array of classification types (e.g., "translation", "quality of life", "qol", "addon", "overhaul", "mod", "expansion", "graphics", "texture", "audio", "ui", "bugfix", "patch") - "genres": Array of literary themes mentioned. -- "themes": Array of literary genres mentioned.- "compatible_items": Array of compatible item names (only if explicitly mentioned; otherwise empty array) +- "themes": Array of literary genres mentioned. +- "compatible_items": Array of compatible item names (only if explicitly mentioned; otherwise empty array) Rules: 1. For "types", infer from description when not explicitly stated (e.g., a UI improvement is "ui" or "quality of life") diff --git a/src/language.rs b/src/language.rs index e02bfeb..0683c64 100644 --- a/src/language.rs +++ b/src/language.rs @@ -78,9 +78,8 @@ pub fn detect(text: &str) -> Vec { } detected_languages .into_iter() - .filter_map(|(lang, words)| { - (words as f32 > total_words as f32 * WORD_PERCENTAGE).then(|| lang.into()) - }) + .filter(|&(lang, words)| (words as f32 > total_words as f32 * WORD_PERCENTAGE)) + .map(|(lang, words)| lang.into()) .collect() } diff --git a/ui/project.inlang/settings.json b/ui/project.inlang/settings.json index 6cec245..de07c06 100644 --- a/ui/project.inlang/settings.json +++ b/ui/project.inlang/settings.json @@ -1,12 +1,12 @@ { - "$schema": "https://inlang.com/schema/project-settings", - "modules": [ - "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", - "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" - ], - "plugin.inlang.messageFormat": { - "pathPattern": "./messages/{locale}.json" - }, - "baseLocale": "en", - "locales": ["en", "es", "de", "ch", "ru"] + "$schema": "https://inlang.com/schema/project-settings", + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + }, + "baseLocale": "en", + "locales": ["en", "es", "de", "ch", "ru"] } From ecdb1e7936d21fcceaf2228349360872997a563f Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 20:45:59 +1200 Subject: [PATCH 15/19] fix: Violating Svelte's unique key on each for properties --- ui/src/routes/app/[id]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/routes/app/[id]/+page.svelte b/ui/src/routes/app/[id]/+page.svelte index 3dd0113..afb9553 100644 --- a/ui/src/routes/app/[id]/+page.svelte +++ b/ui/src/routes/app/[id]/+page.svelte @@ -347,7 +347,7 @@ {#if item.properties && item.properties.length > 0}
- {#each item.properties as prop (prop.id)} + {#each item.properties as prop} {@debug prop} Date: Tue, 15 Jul 2025 20:54:12 +1200 Subject: [PATCH 16/19] fix: Add fallback images for banners --- ui/src/routes/app/[id]/+page.svelte | 3 ++- ui/src/routes/item/[item]/+page.svelte | 2 ++ ui/src/routes/item/[item]/Companion.svelte | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/src/routes/app/[id]/+page.svelte b/ui/src/routes/app/[id]/+page.svelte index afb9553..5254269 100644 --- a/ui/src/routes/app/[id]/+page.svelte +++ b/ui/src/routes/app/[id]/+page.svelte @@ -310,6 +310,7 @@ alt="banner" class:hue-rotate-90={!item.preview_url} class:grayscale={!item.preview_url} + onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} />
@@ -352,7 +353,7 @@ {/each} diff --git a/ui/src/routes/item/[item]/+page.svelte b/ui/src/routes/item/[item]/+page.svelte index e3a57c6..83c61a2 100644 --- a/ui/src/routes/item/[item]/+page.svelte +++ b/ui/src/routes/item/[item]/+page.svelte @@ -191,6 +191,7 @@ loading="lazy" class="aspect-[21/9] w-full object-cover grayscale hue-rotate-90 hover:filter-none" class:hidden={compact} + onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} />
{item.title}
@@ -759,6 +760,7 @@ loading="lazy" class="aspect-[21/9] w-full object-cover grayscale hue-rotate-90 hover:filter-none" class:hidden={false} + onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} />
{item.title} diff --git a/ui/src/routes/item/[item]/Companion.svelte b/ui/src/routes/item/[item]/Companion.svelte index ebfcb6a..566ef58 100644 --- a/ui/src/routes/item/[item]/Companion.svelte +++ b/ui/src/routes/item/[item]/Companion.svelte @@ -24,6 +24,7 @@ loading="lazy" class="aspect-[21/9] w-full object-cover grayscale hue-rotate-90 hover:filter-none" class:hidden={false} + onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} />
{item.title} From 34d804979b14ecd52af2ad474bc1c53b533ba080 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 21:15:46 +1200 Subject: [PATCH 17/19] chore: Tidying --- src/db.rs | 2 +- src/web/properties.rs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/db.rs b/src/db.rs index a835ebd..eff3bdf 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,4 @@ use macros::define_id; define_id!("users", UserID, String); -define_id!("workshop_items", ItemID, String); \ No newline at end of file +define_id!("workshop_items", ItemID, String); diff --git a/src/web/properties.rs b/src/web/properties.rs index 5d875fc..1d01f4c 100644 --- a/src/web/properties.rs +++ b/src/web/properties.rs @@ -3,7 +3,6 @@ use chrono::{DateTime, Utc}; use log::{debug, error}; use salvo::{ Depot, Response, Writer, - fuse::flex::Guard, oapi::extract::JsonBody, prelude::{StatusCode, ToSchema, endpoint}, }; @@ -30,8 +29,6 @@ pub async fn vote(vote_data: JsonBody, depot: &mut Depot, response: &m .await .expect("already authenticated"); let user = UserID::from(user); - &user; - &vote_data; let query = db // .query("BEGIN TRANSACTION;") .query("LET $link = properties:{class: $class, value: $value}") @@ -68,7 +65,7 @@ pub async fn vote(vote_data: JsonBody, depot: &mut Depot, response: &m let result = query.await; match result { Ok(e) => match e.check() { - Ok(v) => { + Ok(_) => { response.status_code(StatusCode::NO_CONTENT); } Err(err) => { @@ -113,7 +110,7 @@ pub async fn remove(vote_data: JsonBody, depot: &mut Depot, response: .await; match result { Ok(e) => match e.check() { - Ok(v) => { + Ok(_) => { response.status_code(StatusCode::NO_CONTENT); } Err(err) => { From cee57ccc879625c6d56c5a6f443fb8302aa8fe96 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 21:21:10 +1200 Subject: [PATCH 18/19] chore(UI): Formatting --- ui/src/routes/admin/+page.svelte | 184 +++++++++++---------- ui/src/routes/admin/+page.ts | 1 - ui/src/routes/app/[id]/+page.svelte | 4 +- ui/src/routes/item/[item]/+page.svelte | 8 +- ui/src/routes/item/[item]/Companion.svelte | 4 +- 5 files changed, 105 insertions(+), 96 deletions(-) diff --git a/ui/src/routes/admin/+page.svelte b/ui/src/routes/admin/+page.svelte index acca313..9954e9b 100644 --- a/ui/src/routes/admin/+page.svelte +++ b/ui/src/routes/admin/+page.svelte @@ -6,8 +6,8 @@ let properties = $state([]); let users = $state([]); - data.users.then(data => users.push(...data)); - data.properties.then(data => properties.push(...data)); + data.users.then((data) => users.push(...data)); + data.properties.then((data) => properties.push(...data)); let searchTerm = ''; let statusFilter = 'all'; @@ -31,7 +31,7 @@ item: prop.in, class: prop.out.class, value: prop.out.value, - status: status, + status: status }) }); if (!res.ok) { @@ -95,70 +95,70 @@ - - - - - - - - + + + + + + + + - {#each properties as property} - {@debug property} - - - - - - - + + + + + - - {/each} + onclick={() => togglePropertyStatus(property, 1)} + disabled={property.status === 1} + >Approve + + + + + {/each}
IDClassValueSubmitted ByStatusActions
IDClassValueSubmitted ByStatusActions
{property.in}{property.out.class}{property.out.value}{property.source} - {#if property.status === -1} - Denied - {:else if property.status === 0} - Pending - {:else} - Approved - {/if} - - {property.in}{property.out.class}{property.out.value}{property.source} + {#if property.status === -1} + Denied + {:else if property.status === 0} + Pending + {:else} + Approved + {/if} + + -
@@ -167,43 +167,45 @@ - - - - - - - + + + + + + + - {#each users as user} - {@debug user} - - - - - - - - {/each} + {#each users as user} + {@debug user} + + + + + + + + {/each}
IDAdminBannedActionsLast Logged In
IDAdminBannedActionsLast Logged In
{user.id} - toggleUserAdmin(user.id, e.target.checked)} - /> - - {toggleUserBan(user.id, e.target.checked)}} - /> - - {user.last_logged_in} - - -
{user.id} + toggleUserAdmin(user.id, e.target.checked)} + /> + + { + toggleUserBan(user.id, e.target.checked); + }} + /> + + {user.last_logged_in} + + +
diff --git a/ui/src/routes/admin/+page.ts b/ui/src/routes/admin/+page.ts index 119c7d1..7595b4b 100644 --- a/ui/src/routes/admin/+page.ts +++ b/ui/src/routes/admin/+page.ts @@ -1,4 +1,3 @@ - export const prerender = false; export const load: PageLoad = async ({ fetch, params }) => { return { diff --git a/ui/src/routes/app/[id]/+page.svelte b/ui/src/routes/app/[id]/+page.svelte index 5254269..f79bd8f 100644 --- a/ui/src/routes/app/[id]/+page.svelte +++ b/ui/src/routes/app/[id]/+page.svelte @@ -310,7 +310,9 @@ alt="banner" class:hue-rotate-90={!item.preview_url} class:grayscale={!item.preview_url} - onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} + onerror={(e) => + (e.target.src = + 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189')} />
diff --git a/ui/src/routes/item/[item]/+page.svelte b/ui/src/routes/item/[item]/+page.svelte index 83c61a2..ea2ef56 100644 --- a/ui/src/routes/item/[item]/+page.svelte +++ b/ui/src/routes/item/[item]/+page.svelte @@ -191,7 +191,9 @@ loading="lazy" class="aspect-[21/9] w-full object-cover grayscale hue-rotate-90 hover:filter-none" class:hidden={compact} - onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} + onerror={(e) => + (e.target.src = + 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189')} />
{item.title}
@@ -760,7 +762,9 @@ loading="lazy" class="aspect-[21/9] w-full object-cover grayscale hue-rotate-90 hover:filter-none" class:hidden={false} - onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} + onerror={(e) => + (e.target.src = + 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189')} />
{item.title} diff --git a/ui/src/routes/item/[item]/Companion.svelte b/ui/src/routes/item/[item]/Companion.svelte index 566ef58..1a56ef1 100644 --- a/ui/src/routes/item/[item]/Companion.svelte +++ b/ui/src/routes/item/[item]/Companion.svelte @@ -24,7 +24,9 @@ loading="lazy" class="aspect-[21/9] w-full object-cover grayscale hue-rotate-90 hover:filter-none" class:hidden={false} - onerror={(e) => e.target.src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189"} + onerror={(e) => + (e.target.src = + 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/294100/header.jpg?t=1734154189')} />
{item.title} From 28956ac5e779f1486975276deda0253ea85562ce Mon Sep 17 00:00:00 2001 From: James Kerr Date: Tue, 15 Jul 2025 21:23:34 +1200 Subject: [PATCH 19/19] ops: Remote lint.yml They're all broken removing it for now --- .github/workflows/lint.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 1efbdd0..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Format Check" -on: - push: - pull_request: - -jobs: - formatting: - name: cargo fmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - # Ensure rustfmt is installed and setup problem matcher - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - components: rustfmt - - name: Rustfmt Check - uses: actions-rust-lang/rustfmt@v1 \ No newline at end of file