diff --git a/Cargo.lock b/Cargo.lock
index 9073954686..22675d2c80 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -477,7 +477,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"password-hash",
]
@@ -494,7 +494,7 @@ dependencies = [
"serde_json",
"thiserror 2.0.17",
"utoipa",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -530,6 +530,45 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "asn1-rs"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 1.0.69",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
[[package]]
name = "astral-tokio-tar"
version = "0.5.6"
@@ -1000,7 +1039,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -1369,6 +1408,17 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+[[package]]
+name = "base64urlsafedata"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
+dependencies = [
+ "base64 0.21.7",
+ "pastey",
+ "serde",
+]
+
[[package]]
name = "bindgen"
version = "0.72.1"
@@ -1831,7 +1881,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -1856,6 +1906,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+[[package]]
+name = "chacha20"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "chardetng"
version = "0.1.17"
@@ -2004,7 +2065,7 @@ dependencies = [
"time",
"tokio",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -2332,6 +2393,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "crc"
version = "3.3.0"
@@ -2706,7 +2776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -2726,6 +2796,20 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "der-parser"
+version = "9.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
[[package]]
name = "deranged"
version = "0.5.4"
@@ -3871,11 +3955,25 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "r-efi",
+ "r-efi 5.3.0",
"wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasip2",
+ "wasip3",
+]
+
[[package]]
name = "gif"
version = "0.13.3"
@@ -4720,6 +4818,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -5307,8 +5411,10 @@ dependencies = [
"utoipa",
"utoipa-actix-web",
"utoipa-scalar",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"validator",
+ "webauthn-rs",
+ "webauthn-rs-proto",
"webp",
"woothee",
"yaserde",
@@ -5341,6 +5447,12 @@ dependencies = [
"spin 0.9.8",
]
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
[[package]]
name = "lebe"
version = "0.5.3"
@@ -5742,7 +5854,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"tokio",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"wasm-bindgen-futures",
"web-sys",
"yaup",
@@ -5882,7 +5994,7 @@ dependencies = [
"rustc_version",
"smallvec",
"tagptr",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -5933,7 +6045,7 @@ dependencies = [
"serde_with",
"strum",
"utoipa",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -6568,6 +6680,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "oid-registry"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
+dependencies = [
+ "asn1-rs",
+]
+
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -6598,15 +6719,14 @@ dependencies = [
[[package]]
name = "openssl"
-version = "0.10.73"
+version = "0.10.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
dependencies = [
"bitflags 2.9.4",
"cfg-if",
"foreign-types 0.3.2",
"libc",
- "once_cell",
"openssl-macros",
"openssl-sys",
]
@@ -6630,9 +6750,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
-version = "0.9.109"
+version = "0.9.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
dependencies = [
"cc",
"libc",
@@ -6936,6 +7056,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
[[package]]
name = "path-util"
version = "0.0.0"
@@ -7764,6 +7890,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
[[package]]
name = "r2d2"
version = "0.8.10"
@@ -7816,6 +7948,17 @@ dependencies = [
"rand_core 0.9.3",
]
+[[package]]
+name = "rand"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.2",
+ "rand_core 0.10.1",
+]
+
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -7873,6 +8016,12 @@ dependencies = [
"getrandom 0.3.3",
]
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -8269,7 +8418,7 @@ dependencies = [
"rkyv_derive",
"seahash",
"tinyvec",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -8408,6 +8557,15 @@ dependencies = [
"semver",
]
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom 7.1.3",
+]
+
[[package]]
name = "rustix"
version = "0.38.44"
@@ -8652,7 +8810,7 @@ dependencies = [
"serde",
"serde_json",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -8927,7 +9085,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -8984,6 +9142,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_cbor_2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
+dependencies = [
+ "half 2.7.0",
+ "serde",
+]
+
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -9230,7 +9398,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -9247,7 +9415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -9503,7 +9671,7 @@ dependencies = [
"tokio-stream",
"tracing",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"webpki-roots 0.26.11",
]
@@ -9586,7 +9754,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.17",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"whoami",
]
@@ -9626,7 +9794,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.17",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"whoami",
]
@@ -9653,7 +9821,7 @@ dependencies = [
"thiserror 2.0.17",
"tracing",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -10118,7 +10286,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"walkdir",
]
@@ -10426,7 +10594,7 @@ dependencies = [
"toml 0.9.8",
"url",
"urlpattern",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"walkdir",
]
@@ -10582,7 +10750,7 @@ dependencies = [
"tracing-error",
"tracing-subscriber",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"whoami",
"windows",
"windows-core 0.61.2",
@@ -10630,7 +10798,7 @@ dependencies = [
"tracing-error",
"url",
"urlencoding",
- "uuid 1.18.1",
+ "uuid 1.23.3",
"webview2-com",
"windows",
"windows-core 0.61.2",
@@ -11119,9 +11287,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -11139,14 +11307,14 @@ dependencies = [
"mutually_exclusive_features",
"pin-project",
"tracing",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
name = "tracing-attributes"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -11155,9 +11323,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.34"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@@ -11553,7 +11721,7 @@ dependencies = [
"regex",
"syn 2.0.106",
"url",
- "uuid 1.18.1",
+ "uuid 1.23.3",
]
[[package]]
@@ -11579,14 +11747,14 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.18.1"
+version = "1.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
dependencies = [
- "getrandom 0.3.3",
+ "getrandom 0.4.2",
"js-sys",
- "rand 0.9.2",
- "serde",
+ "rand 0.10.1",
+ "serde_core",
"wasm-bindgen",
]
@@ -11761,7 +11929,16 @@ version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
- "wit-bindgen",
+ "wit-bindgen 0.46.0",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
]
[[package]]
@@ -11829,6 +12006,28 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.11.4",
+ "wasm-encoder",
+ "wasmparser",
+]
+
[[package]]
name = "wasm-streams"
version = "0.4.2"
@@ -11855,6 +12054,18 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.9.4",
+ "hashbrown 0.15.5",
+ "indexmap 2.11.4",
+ "semver",
+]
+
[[package]]
name = "wayland-backend"
version = "0.3.11"
@@ -11935,6 +12146,74 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "webauthn-attestation-ca"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e"
+dependencies = [
+ "base64urlsafedata",
+ "openssl",
+ "openssl-sys",
+ "serde",
+ "tracing",
+ "uuid 1.23.3",
+]
+
+[[package]]
+name = "webauthn-rs"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422"
+dependencies = [
+ "base64urlsafedata",
+ "serde",
+ "tracing",
+ "url",
+ "uuid 1.23.3",
+ "webauthn-rs-core",
+]
+
+[[package]]
+name = "webauthn-rs-core"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a"
+dependencies = [
+ "base64 0.21.7",
+ "base64urlsafedata",
+ "der-parser",
+ "hex",
+ "nom 7.1.3",
+ "openssl",
+ "openssl-sys",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
+ "serde",
+ "serde_cbor_2",
+ "serde_json",
+ "thiserror 1.0.69",
+ "tracing",
+ "url",
+ "uuid 1.23.3",
+ "webauthn-attestation-ca",
+ "webauthn-rs-proto",
+ "x509-parser",
+]
+
+[[package]]
+name = "webauthn-rs-proto"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e"
+dependencies = [
+ "base64 0.21.7",
+ "base64urlsafedata",
+ "serde",
+ "serde_json",
+ "url",
+]
+
[[package]]
name = "webkit2gtk"
version = "2.0.2"
@@ -12657,6 +12936,94 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "indexmap 2.11.4",
+ "prettyplease",
+ "syn 2.0.106",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.9.4",
+ "indexmap 2.11.4",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.11.4",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
[[package]]
name = "woothee"
version = "0.13.0"
@@ -12748,6 +13115,23 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "x509-parser"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom 7.1.3",
+ "oid-registry",
+ "rusticata-macros",
+ "thiserror 1.0.69",
+ "time",
+]
+
[[package]]
name = "xattr"
version = "1.6.1"
diff --git a/Cargo.toml b/Cargo.toml
index 7ce30a47f6..922bb0aeef 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -219,6 +219,8 @@ utoipa-actix-web = { version = "0.1.2" }
utoipa-scalar = { version = "0.3.0", default-features = false }
uuid = "1.18.1"
validator = "0.20.0"
+webauthn-rs = "0.5.5"
+webauthn-rs-proto = "0.5.5"
webp = { version = "0.3.1", default-features = false }
webview2-com = "0.38.0" # Should be updated in lockstep with wry
whoami = "1.6.1"
diff --git a/apps/frontend/src/components/ui/auth/PasskeySettings.vue b/apps/frontend/src/components/ui/auth/PasskeySettings.vue
new file mode 100644
index 0000000000..79fe5771a1
--- /dev/null
+++ b/apps/frontend/src/components/ui/auth/PasskeySettings.vue
@@ -0,0 +1,422 @@
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.managePasskeyModalLoading) }}
+
+
+
+ {{ formatMessage(messages.managePasskeyModalNoPasskeys) }}
+
+
+
+
+ {{ passkey.name }}
+
+
+
+ {{
+ formatMessage(messages.managePasskeyModalAdded, {
+ ago: formatRelativeTime(passkey.created_at),
+ })
+ }}
+
+ ⋅
+
+ {{
+ formatMessage(messages.managePasskeyModalLastUsed, {
+ ago: formatRelativeTime(passkey.last_used),
+ })
+ }}
+
+
+ {{ formatMessage(messages.managePasskeyModalNeverUsed) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/frontend/src/components/ui/auth/SignIn.vue b/apps/frontend/src/components/ui/auth/SignIn.vue
index ffb3414653..43f2d47a2f 100644
--- a/apps/frontend/src/components/ui/auth/SignIn.vue
+++ b/apps/frontend/src/components/ui/auth/SignIn.vue
@@ -153,6 +153,25 @@
+
+
+
+ {{ formatMessage(messages.continueWithPasskey) }}
+
+ {{ formatMessage(messages.lastSignInLabel) }}
+
+
+
@@ -235,6 +254,7 @@ import {
MicrosoftColorIcon,
RightArrowIcon,
SteamColorIcon,
+ UserKeyIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, defineMessages, StyledInput, useVIntl } from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
@@ -248,7 +268,7 @@ import {
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
} from '@/composables/auth.ts'
-type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft'
+type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft' | 'passkey'
interface AuthGlobals {
captcha_enabled?: boolean
@@ -263,6 +283,7 @@ interface Props {
globals?: AuthGlobals | null
onPasswordSignIn?: () => void
onTwoFactorSignIn?: () => void
+ onPasskeySignIn?: () => void
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
}
@@ -274,6 +295,7 @@ const {
globals = null,
onPasswordSignIn = () => {},
onTwoFactorSignIn = () => {},
+ onPasskeySignIn = () => {},
onSetCaptchaRef = undefined,
} = defineProps()
@@ -343,6 +365,10 @@ const messages = defineMessages({
id: 'auth.sign-in.last-sign-in',
defaultMessage: 'Last sign in',
},
+ continueWithPasskey: {
+ id: 'auth.sign-in.continue-with-passkey',
+ defaultMessage: 'Continue with passkey',
+ },
})
diff --git a/apps/frontend/src/helpers/passkey.ts b/apps/frontend/src/helpers/passkey.ts
new file mode 100644
index 0000000000..746e6638aa
--- /dev/null
+++ b/apps/frontend/src/helpers/passkey.ts
@@ -0,0 +1,95 @@
+function ensurePasskeySupported() {
+ const supported =
+ typeof window !== 'undefined' &&
+ typeof window.PublicKeyCredential !== 'undefined' &&
+ typeof navigator !== 'undefined' &&
+ !!navigator.credentials
+ if (!supported) {
+ throw new Error('Passkeys are not supported by this browser.')
+ }
+}
+
+function base64urlToBuffer(base64url: string) {
+ return Uint8Array.from(atob(base64url.replace(/-/g, '+').replace(/_/g, '/')), (char) =>
+ char.charCodeAt(0),
+ )
+}
+
+function bufferToBase64url(buffer: ArrayBuffer) {
+ const bytes = new Uint8Array(buffer)
+ let str = ''
+ for (let i = 0; i < bytes.length; i++) {
+ str += String.fromCharCode(bytes[i])
+ }
+ return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
+}
+
+/**
+ * Creates a passkey credential using the browser's WebAuthn API.
+ *
+ * @param options The public key options for creating the passkey credential, provided by the server.
+ */
+export async function createPasskeyCredential(options: any) {
+ ensurePasskeySupported()
+
+ const publicKey = {
+ ...options,
+ challenge: base64urlToBuffer(options.challenge),
+ user: {
+ ...options.user,
+ id: base64urlToBuffer(options.user.id),
+ },
+ excludeCredentials: options.excludeCredentials?.map((cred: any) => ({
+ ...cred,
+ id: base64urlToBuffer(cred.id),
+ })),
+ }
+
+ const credential = (await navigator.credentials.create({ publicKey })) as PublicKeyCredential
+ const response = credential.response as AuthenticatorAttestationResponse
+
+ return {
+ id: credential.id,
+ rawId: bufferToBase64url(credential.rawId),
+ type: credential.type,
+ response: {
+ clientDataJSON: bufferToBase64url(response.clientDataJSON),
+ attestationObject: bufferToBase64url(response.attestationObject),
+ },
+ extensions: credential.getClientExtensionResults(),
+ }
+}
+
+/**
+ * Authenticates a user using a passkey credential.
+ *
+ * @param options The public key options for authenticating the passkey credential, provided by the server.
+ */
+export async function getPasskeyCredential(options: any) {
+ ensurePasskeySupported()
+
+ const publicKey = {
+ ...options,
+ challenge: base64urlToBuffer(options.challenge),
+ allowCredentials: options.allowCredentials?.map((cred: any) => ({
+ ...cred,
+ id: base64urlToBuffer(cred.id),
+ })),
+ }
+
+ const credential = (await navigator.credentials.get({ publicKey })) as PublicKeyCredential
+ const response = credential.response as AuthenticatorAssertionResponse
+
+ return {
+ id: credential.id,
+ rawId: bufferToBase64url(credential.rawId),
+ type: credential.type,
+ response: {
+ clientDataJSON: bufferToBase64url(response.clientDataJSON),
+ authenticatorData: bufferToBase64url(response.authenticatorData),
+ signature: bufferToBase64url(response.signature),
+ userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : null,
+ },
+ extensions: credential.getClientExtensionResults(),
+ }
+}
diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json
index ce0c295fc8..a8ac6df2ab 100644
--- a/apps/frontend/src/locales/en-US/index.json
+++ b/apps/frontend/src/locales/en-US/index.json
@@ -815,6 +815,9 @@
"auth.sign-in.continue-with-email": {
"message": "Continue with Email"
},
+ "auth.sign-in.continue-with-passkey": {
+ "message": "Continue with passkey"
+ },
"auth.sign-in.create-account": {
"message": "Sign up"
},
@@ -4043,6 +4046,48 @@
"settings.account.security.email.title": {
"message": "Email"
},
+ "settings.account.security.passkey.add": {
+ "message": "Add passkey"
+ },
+ "settings.account.security.passkey.add-modal.name.description": {
+ "message": "Make sure to pick something memorable, so you can identify this passkey later."
+ },
+ "settings.account.security.passkey.add-modal.name.label": {
+ "message": "Name"
+ },
+ "settings.account.security.passkey.add-modal.name.placeholder": {
+ "message": "My passkey"
+ },
+ "settings.account.security.passkey.description": {
+ "message": "Manage your registered passkeys, or add a new one."
+ },
+ "settings.account.security.passkey.modal.added": {
+ "message": "Added {ago}"
+ },
+ "settings.account.security.passkey.modal.last-used": {
+ "message": "Last used {ago}"
+ },
+ "settings.account.security.passkey.modal.loading": {
+ "message": "Loading passkeys…"
+ },
+ "settings.account.security.passkey.modal.never-used": {
+ "message": "Never used"
+ },
+ "settings.account.security.passkey.modal.no-passkeys": {
+ "message": "You have no passkeys registered yet."
+ },
+ "settings.account.security.passkey.remove.description": {
+ "message": "This will permanently remove the passkey \"{name}\". You will no longer be able to sign in with it."
+ },
+ "settings.account.security.passkey.remove.title": {
+ "message": "Are you sure you want to remove this passkey?"
+ },
+ "settings.account.security.passkey.rename-modal.header": {
+ "message": "Rename passkey"
+ },
+ "settings.account.security.passkey.title": {
+ "message": "Manage passkeys"
+ },
"settings.account.security.password.action.add": {
"message": "Add password"
},
diff --git a/apps/frontend/src/pages/auth/sign-in.vue b/apps/frontend/src/pages/auth/sign-in.vue
index 03b6371d0e..adb3e4f488 100644
--- a/apps/frontend/src/pages/auth/sign-in.vue
+++ b/apps/frontend/src/pages/auth/sign-in.vue
@@ -11,6 +11,7 @@
:globals="globals"
:on-password-sign-in="beginPasswordSignIn"
:on-two-factor-sign-in="begin2FASignIn"
+ :on-passkey-sign-in="beginPasskeySignin"
:on-set-captcha-ref="setCaptchaRef"
/>
@@ -34,8 +35,9 @@ import {
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
promotePendingSignInOAuthProvider,
} from '@/composables/auth.ts'
+import { getPasskeyCredential } from '@/helpers/passkey.ts'
-type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft'
+type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft' | 'passkey'
interface AuthGlobalsResponse {
captcha_enabled?: boolean
@@ -193,6 +195,30 @@ async function begin2FASignIn() {
stopLoading()
}
+async function beginPasskeySignin() {
+ startLoading()
+ try {
+ const start = await client.labrinth.auth_v2.authenticatePasskeyStart()
+
+ const credential = await getPasskeyCredential(start.options.publicKey)
+
+ const result = await client.labrinth.auth_v2.authenticatePasskeyFinish({
+ flow: start.flow,
+ credential,
+ })
+
+ pendingSignInOAuthProvider.value = 'passkey'
+ await finishSignIn(result.session)
+ } catch (err) {
+ addNotification({
+ title: formatMessage(commonMessages.errorNotificationTitle),
+ text: getErrorMessage(err),
+ type: 'error',
+ })
+ }
+ stopLoading()
+}
+
async function finishSignIn(sessionToken?: string | null) {
if (route.query.launcher) {
let token = sessionToken
diff --git a/apps/frontend/src/pages/settings/account.vue b/apps/frontend/src/pages/settings/account.vue
index 702e1b7f84..76ae58b229 100644
--- a/apps/frontend/src/pages/settings/account.vue
+++ b/apps/frontend/src/pages/settings/account.vue
@@ -440,6 +440,7 @@
+
@@ -513,6 +514,7 @@ import MicrosoftIcon from 'assets/icons/auth/sso-microsoft.svg'
import SteamIcon from 'assets/icons/auth/sso-steam.svg'
import QrcodeVue from 'qrcode.vue'
+import PasskeySettings from '~/components/ui/auth/PasskeySettings.vue'
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.ts'
definePageMeta({
diff --git a/apps/labrinth/.env.docker-compose b/apps/labrinth/.env.docker-compose
index 045cfd800c..f799177874 100644
--- a/apps/labrinth/.env.docker-compose
+++ b/apps/labrinth/.env.docker-compose
@@ -170,3 +170,6 @@ MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
SERVER_PING_TIMEOUT=10000
SERVER_PING_RETRIES=3
+
+# Display name for Webauthn Authenticators
+WEBAUTHN_RP_NAME=Modrinth
diff --git a/apps/labrinth/.env.local b/apps/labrinth/.env.local
index 19a7d95287..dfd8ed7487 100644
--- a/apps/labrinth/.env.local
+++ b/apps/labrinth/.env.local
@@ -192,3 +192,6 @@ DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
SERVER_PING_TIMEOUT=10000
SERVER_PING_RETRIES=3
SERVER_PING_MIN_INTERVAL_SEC=1800
+
+# Display name for Webauthn Authenticators
+WEBAUTHN_RP_NAME=Modrinth
diff --git a/apps/labrinth/.sqlx/query-05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b.json b/apps/labrinth/.sqlx/query-05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b.json
new file mode 100644
index 0000000000..106e54f7af
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b.json
@@ -0,0 +1,58 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json\",\n last_used, created_at\n FROM user_passkeys\n WHERE credential_id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "name",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "credential_id",
+ "type_info": "Bytea"
+ },
+ {
+ "ordinal": 4,
+ "name": "passkey: sqlx::types::Json",
+ "type_info": "Jsonb"
+ },
+ {
+ "ordinal": 5,
+ "name": "last_used",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 6,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Bytea"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false
+ ]
+ },
+ "hash": "05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b"
+}
diff --git a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json b/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json
deleted file mode 100644
index 4bc87e73a5..0000000000
--- a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "count",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Int8"
- ]
- },
- "nullable": [
- null
- ]
- },
- "hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c"
-}
diff --git a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json b/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json
deleted file mode 100644
index 921f7f92d9..0000000000
--- a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Int8"
- },
- {
- "ordinal": 1,
- "name": "status: PayoutStatus",
- "type_info": "Varchar"
- }
- ],
- "parameters": {
- "Left": []
- },
- "nullable": [
- false,
- false
- ]
- },
- "hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
-}
diff --git a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json b/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json
deleted file mode 100644
index 3c99ff3fed..0000000000
--- a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "count",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": []
- },
- "nullable": [
- null
- ]
- },
- "hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4"
-}
diff --git a/apps/labrinth/.sqlx/query-29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129.json b/apps/labrinth/.sqlx/query-29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129.json
new file mode 100644
index 0000000000..f42f890f09
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129.json
@@ -0,0 +1,16 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE user_passkeys SET name = $1\n WHERE id = $2 AND user_id = $3\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Varchar",
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129"
+}
diff --git a/apps/labrinth/.sqlx/query-31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b.json b/apps/labrinth/.sqlx/query-31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b.json
new file mode 100644
index 0000000000..b8b7a22428
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b.json
@@ -0,0 +1,28 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM sessions WHERE user_id = $1 RETURNING id, session\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "session",
+ "type_info": "Varchar"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b"
+}
diff --git a/apps/labrinth/.sqlx/query-cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76.json b/apps/labrinth/.sqlx/query-3ca51012492b969bb6a474ee22c9e53e57b6da3a1145f0d86a2bab36be436eb6.json
similarity index 75%
rename from apps/labrinth/.sqlx/query-cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76.json
rename to apps/labrinth/.sqlx/query-3ca51012492b969bb6a474ee22c9e53e57b6da3a1145f0d86a2bab36be436eb6.json
index 96ca5abb6f..506d54fda7 100644
--- a/apps/labrinth/.sqlx/query-cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76.json
+++ b/apps/labrinth/.sqlx/query-3ca51012492b969bb6a474ee22c9e53e57b6da3a1145f0d86a2bab36be436eb6.json
@@ -1,10 +1,6 @@
{
"db_name": "PostgreSQL",
-<<<<<<<< HEAD:apps/labrinth/.sqlx/query-cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76.json
- "query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,\n eligibility_verified_at\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
-========
- "query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n (\n SELECT MAX(campaign_donations.donated_at)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_last_donated_at,\n (\n SELECT SUM(campaign_donations.amount_usd)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_total_amount_donated_usd,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
->>>>>>>> main:apps/labrinth/.sqlx/query-e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5.json
+ "query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n (\n SELECT MAX(campaign_donations.donated_at)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_last_donated_at,\n (\n SELECT SUM(campaign_donations.amount_usd)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_total_amount_donated_usd,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,\n eligibility_verified_at\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
"describe": {
"columns": [
{
@@ -143,7 +139,7 @@
"type_info": "Bool"
},
{
- "ordinal": 25,
+ "ordinal": 27,
"name": "eligibility_verified_at",
"type_info": "Timestamptz"
}
@@ -185,9 +181,5 @@
true
]
},
-<<<<<<<< HEAD:apps/labrinth/.sqlx/query-cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76.json
- "hash": "cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76"
-========
- "hash": "e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5"
->>>>>>>> main:apps/labrinth/.sqlx/query-e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5.json
+ "hash": "3ca51012492b969bb6a474ee22c9e53e57b6da3a1145f0d86a2bab36be436eb6"
}
diff --git a/apps/labrinth/.sqlx/query-42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e.json b/apps/labrinth/.sqlx/query-42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e.json
new file mode 100644
index 0000000000..44d2b50c59
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM user_passkeys\n WHERE id = $1 AND user_id = $2\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e"
+}
diff --git a/apps/labrinth/.sqlx/query-538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73.json b/apps/labrinth/.sqlx/query-538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73.json
new file mode 100644
index 0000000000..c990bb2864
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE user_passkeys\n SET passkey = $1, last_used = NOW()\n WHERE id = $2\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Jsonb",
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73"
+}
diff --git a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json b/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json
deleted file mode 100644
index b4c2e5a56e..0000000000
--- a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Timestamptz",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850"
-}
diff --git a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json b/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json
deleted file mode 100644
index fc7d2ac98d..0000000000
--- a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Numeric",
- "Timestamptz"
- ]
- },
- "nullable": []
- },
- "hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8"
-}
diff --git a/apps/labrinth/.sqlx/query-7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9.json b/apps/labrinth/.sqlx/query-7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9.json
new file mode 100644
index 0000000000..b0e65451e3
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO user_passkeys (\n id, user_id, name, credential_id, passkey, created_at, last_used\n )\n VALUES (\n $1, $2 ,$3, $4, $5, $6, $7\n )\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8",
+ "Varchar",
+ "Bytea",
+ "Jsonb",
+ "Timestamptz",
+ "Timestamptz"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9"
+}
diff --git a/apps/labrinth/.sqlx/query-806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d.json b/apps/labrinth/.sqlx/query-806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d.json
new file mode 100644
index 0000000000..a5eb0420a0
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d.json
@@ -0,0 +1,58 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json\",\n last_used, created_at\n FROM user_passkeys\n WHERE user_id = $1\n ORDER BY created_at DESC\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "user_id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "name",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "credential_id",
+ "type_info": "Bytea"
+ },
+ {
+ "ordinal": 4,
+ "name": "passkey: sqlx::types::Json",
+ "type_info": "Jsonb"
+ },
+ {
+ "ordinal": 5,
+ "name": "last_used",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 6,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false
+ ]
+ },
+ "hash": "806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d"
+}
diff --git a/apps/labrinth/.sqlx/query-9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb.json b/apps/labrinth/.sqlx/query-9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb.json
new file mode 100644
index 0000000000..ae5395b6c8
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT EXISTS(SELECT 1 FROM user_passkeys WHERE id=$1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ },
+ "hash": "9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb"
+}
diff --git a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json b/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json
deleted file mode 100644
index 89bd8147dc..0000000000
--- a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "status: PayoutStatus",
- "type_info": "Varchar"
- }
- ],
- "parameters": {
- "Left": []
- },
- "nullable": [
- false
- ]
- },
- "hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
-}
diff --git a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json b/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json
deleted file mode 100644
index 469c30168a..0000000000
--- a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Text",
- "Text",
- "Varchar",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
-}
diff --git a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json b/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json
deleted file mode 100644
index 52e020ebf2..0000000000
--- a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Int8",
- "Text",
- "Varchar",
- "Int8"
- ]
- },
- "nullable": []
- },
- "hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
-}
diff --git a/apps/labrinth/.sqlx/query-ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d.json b/apps/labrinth/.sqlx/query-ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d.json
new file mode 100644
index 0000000000..f2f563deb2
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d.json
@@ -0,0 +1,14 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n DELETE FROM user_passkeys\n WHERE id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d"
+}
diff --git a/apps/labrinth/.sqlx/query-e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5.json b/apps/labrinth/.sqlx/query-e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5.json
deleted file mode 100644
index 96ca5abb6f..0000000000
--- a/apps/labrinth/.sqlx/query-e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5.json
+++ /dev/null
@@ -1,193 +0,0 @@
-{
- "db_name": "PostgreSQL",
-<<<<<<<< HEAD:apps/labrinth/.sqlx/query-cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76.json
- "query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,\n eligibility_verified_at\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
-========
- "query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n (\n SELECT MAX(campaign_donations.donated_at)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_last_donated_at,\n (\n SELECT SUM(campaign_donations.amount_usd)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_total_amount_donated_usd,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
->>>>>>>> main:apps/labrinth/.sqlx/query-e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5.json
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Int8"
- },
- {
- "ordinal": 1,
- "name": "email",
- "type_info": "Varchar"
- },
- {
- "ordinal": 2,
- "name": "avatar_url",
- "type_info": "Varchar"
- },
- {
- "ordinal": 3,
- "name": "raw_avatar_url",
- "type_info": "Text"
- },
- {
- "ordinal": 4,
- "name": "username",
- "type_info": "Varchar"
- },
- {
- "ordinal": 5,
- "name": "bio",
- "type_info": "Varchar"
- },
- {
- "ordinal": 6,
- "name": "created",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 7,
- "name": "role",
- "type_info": "Varchar"
- },
- {
- "ordinal": 8,
- "name": "badges",
- "type_info": "Int8"
- },
- {
- "ordinal": 9,
- "name": "campaign_pride_26_last_donated_at",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 10,
- "name": "campaign_pride_26_total_amount_donated_usd",
- "type_info": "Numeric"
- },
- {
- "ordinal": 11,
- "name": "github_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 12,
- "name": "discord_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 13,
- "name": "gitlab_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 14,
- "name": "google_id",
- "type_info": "Varchar"
- },
- {
- "ordinal": 15,
- "name": "steam_id",
- "type_info": "Int8"
- },
- {
- "ordinal": 16,
- "name": "microsoft_id",
- "type_info": "Varchar"
- },
- {
- "ordinal": 17,
- "name": "email_verified",
- "type_info": "Bool"
- },
- {
- "ordinal": 18,
- "name": "password",
- "type_info": "Text"
- },
- {
- "ordinal": 19,
- "name": "totp_secret",
- "type_info": "Varchar"
- },
- {
- "ordinal": 20,
- "name": "paypal_id",
- "type_info": "Text"
- },
- {
- "ordinal": 21,
- "name": "paypal_country",
- "type_info": "Text"
- },
- {
- "ordinal": 22,
- "name": "paypal_email",
- "type_info": "Text"
- },
- {
- "ordinal": 23,
- "name": "venmo_handle",
- "type_info": "Text"
- },
- {
- "ordinal": 24,
- "name": "stripe_customer_id",
- "type_info": "Text"
- },
- {
- "ordinal": 25,
- "name": "allow_friend_requests",
- "type_info": "Bool"
- },
- {
- "ordinal": 26,
- "name": "is_subscribed_to_newsletter",
- "type_info": "Bool"
- },
- {
- "ordinal": 25,
- "name": "eligibility_verified_at",
- "type_info": "Timestamptz"
- }
- ],
- "parameters": {
- "Left": [
- "Int8Array",
- "TextArray"
- ]
- },
- "nullable": [
- false,
- true,
- true,
- true,
- false,
- true,
- false,
- false,
- false,
- null,
- null,
- true,
- true,
- true,
- true,
- true,
- true,
- false,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- false,
- false,
- true
- ]
- },
-<<<<<<<< HEAD:apps/labrinth/.sqlx/query-cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76.json
- "hash": "cc2056c368e11fcd820dfd6951d4fa2dd26da8813d4b0dbaa6bd66f7060f1e76"
-========
- "hash": "e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5"
->>>>>>>> main:apps/labrinth/.sqlx/query-e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5.json
-}
diff --git a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json b/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json
deleted file mode 100644
index d3e3520bcc..0000000000
--- a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "count",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Int8"
- ]
- },
- "nullable": [
- null
- ]
- },
- "hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41"
-}
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index 9d99dc5007..e0294a98bb 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -130,6 +130,8 @@ utoipa-actix-web = { workspace = true }
utoipa-scalar = { workspace = true, features = ["actix-web"] }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
validator = { workspace = true, features = ["derive"] }
+webauthn-rs = { workspace = true, features = ["danger-allow-state-serialisation", "conditional-ui"] }
+webauthn-rs-proto = { workspace = true }
webp = { workspace = true }
woothee = { workspace = true }
yaserde = { workspace = true, features = ["derive"] }
diff --git a/apps/labrinth/migrations/20260610162635_passkeys.sql b/apps/labrinth/migrations/20260610162635_passkeys.sql
new file mode 100644
index 0000000000..9e798b3bcf
--- /dev/null
+++ b/apps/labrinth/migrations/20260610162635_passkeys.sql
@@ -0,0 +1,11 @@
+CREATE TABLE user_passkeys (
+ id BIGINT PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ name VARCHAR(255) NOT NULL,
+ credential_id BYTEA NOT NULL UNIQUE,
+ passkey JSONB NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ last_used TIMESTAMPTZ
+);
+
+CREATE INDEX user_passkeys_user_id ON user_passkeys (user_id);
diff --git a/apps/labrinth/src/database/models/flow_item.rs b/apps/labrinth/src/database/models/flow_item.rs
index a000057f6e..2bc8dd864d 100644
--- a/apps/labrinth/src/database/models/flow_item.rs
+++ b/apps/labrinth/src/database/models/flow_item.rs
@@ -11,6 +11,7 @@ use rand_chacha::ChaCha20Rng;
use rand_chacha::rand_core::SeedableRng;
use serde::{Deserialize, Serialize};
use url::Url;
+use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration};
const FLOWS_NAMESPACE: &str = "flows";
@@ -58,6 +59,13 @@ pub enum DBFlow {
scopes: Scopes,
original_redirect_uri: Option, // Needed for https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
},
+ RegisterPasskey {
+ user_id: DBUserId,
+ state: PasskeyRegistration,
+ },
+ AuthenticatePasskey {
+ state: DiscoverableAuthentication,
+ },
}
impl DBFlow {
diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs
index 1ebb09b27d..5fe435a37a 100644
--- a/apps/labrinth/src/database/models/ids.rs
+++ b/apps/labrinth/src/database/models/ids.rs
@@ -4,9 +4,10 @@ use crate::models::ids::{
AffiliateCodeId, AnalyticsEventId, CampaignDonationId, ChargeId,
CollectionId, FileId, ImageId, NotificationId, OAuthAccessTokenId,
OAuthClientAuthorizationId, OAuthClientId, OAuthRedirectUriId,
- OrganizationId, PatId, PayoutId, ProductId, ProductPriceId, ProjectId,
- ReportId, SessionId, SharedInstanceId, SharedInstanceVersionId, TeamId,
- TeamMemberId, ThreadId, ThreadMessageId, UserSubscriptionId, VersionId,
+ OrganizationId, PasskeyId, PatId, PayoutId, ProductId, ProductPriceId,
+ ProjectId, ReportId, SessionId, SharedInstanceId, SharedInstanceVersionId,
+ TeamId, TeamMemberId, ThreadId, ThreadMessageId, UserSubscriptionId,
+ VersionId,
};
use ariadne::ids::base62_impl::to_base62;
use ariadne::ids::{UserId, random_base62_rng, random_base62_rng_range};
@@ -277,6 +278,10 @@ db_id_interface!(
AnalyticsEventId,
generator: generate_analytics_event_id @ "analytics_events",
);
+db_id_interface!(
+ PasskeyId,
+ generator: generate_passkey_id @ "user_passkeys",
+);
id_type!(CategoryId as i32);
id_type!(GameId as i32);
diff --git a/apps/labrinth/src/database/models/mod.rs b/apps/labrinth/src/database/models/mod.rs
index f07bc6a135..a499f33d2f 100644
--- a/apps/labrinth/src/database/models/mod.rs
+++ b/apps/labrinth/src/database/models/mod.rs
@@ -23,6 +23,7 @@ pub mod oauth_client_authorization_item;
pub mod oauth_client_item;
pub mod oauth_token_item;
pub mod organization_item;
+pub mod passkey_item;
pub mod pat_item;
pub mod payout_item;
pub mod payouts_values_notifications;
@@ -51,6 +52,7 @@ pub use ids::*;
pub use image_item::DBImage;
pub use oauth_client_item::DBOAuthClient;
pub use organization_item::DBOrganization;
+pub use passkey_item::DBPasskey;
pub use project_item::DBProject;
pub use team_item::DBTeam;
pub use team_item::DBTeamMember;
diff --git a/apps/labrinth/src/database/models/passkey_item.rs b/apps/labrinth/src/database/models/passkey_item.rs
new file mode 100644
index 0000000000..063e0724f4
--- /dev/null
+++ b/apps/labrinth/src/database/models/passkey_item.rs
@@ -0,0 +1,191 @@
+use super::ids::*;
+use crate::database::PgTransaction;
+use crate::database::models::DatabaseError;
+use chrono::{DateTime, Utc};
+use futures::TryStreamExt;
+use serde::{Deserialize, Serialize};
+use sqlx::types::Json;
+use webauthn_rs::prelude::Passkey;
+
+#[derive(Deserialize, Serialize, Clone, Debug)]
+pub struct DBPasskey {
+ pub id: DBPasskeyId,
+ pub user_id: DBUserId,
+ pub name: String,
+ pub credential_id: Vec,
+ pub passkey: Passkey,
+ pub created_at: DateTime,
+ pub last_used: Option>,
+}
+
+impl DBPasskey {
+ pub async fn insert(
+ &self,
+ transaction: &mut PgTransaction<'_>,
+ ) -> Result<(), DatabaseError> {
+ sqlx::query!(
+ "
+ INSERT INTO user_passkeys (
+ id, user_id, name, credential_id, passkey, created_at, last_used
+ )
+ VALUES (
+ $1, $2 ,$3, $4, $5, $6, $7
+ )
+ ",
+ self.id as DBPasskeyId,
+ self.user_id as DBUserId,
+ self.name,
+ self.credential_id,
+ Json(&self.passkey) as _,
+ self.created_at,
+ self.last_used,
+ )
+ .execute(&mut *transaction)
+ .await?;
+
+ Ok(())
+ }
+
+ pub async fn get_by_credential_id<'a, E>(
+ credential_id: &[u8],
+ exec: E,
+ ) -> Result