mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-07-27 11:12:37 +00:00
small bug fixes, code dedup
This commit is contained in:
Generated
+2
-478
@@ -1067,9 +1067,6 @@ name = "bytes"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes-utils"
|
||||
@@ -1378,15 +1375,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
@@ -2069,16 +2057,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
|
||||
dependencies = [
|
||||
"mac",
|
||||
"new_debug_unreachable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -2284,21 +2262,6 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-storage"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
|
||||
dependencies = [
|
||||
"gloo-utils",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
@@ -2311,19 +2274,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-utils"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.4"
|
||||
@@ -2586,20 +2536,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -3127,80 +3063,6 @@ version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
||||
|
||||
[[package]]
|
||||
name = "jacquard"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c1fdbcf1153e6e6b87fde20036c1ffe7473c4852f1c6369bc4ef1fe47ccb9f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.2.16",
|
||||
"gloo-storage",
|
||||
"http 1.4.0",
|
||||
"jacquard-api",
|
||||
"jacquard-common",
|
||||
"jacquard-derive",
|
||||
"jacquard-identity",
|
||||
"jacquard-oauth",
|
||||
"jose-jwk",
|
||||
"miette",
|
||||
"regex",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"smol_str",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"trait-variant",
|
||||
"url",
|
||||
"webpage",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jacquard-api"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979fb1848c1dd7ac8fd12745bc71f56f6da61374407d5f9b06005467a954e5a"
|
||||
dependencies = [
|
||||
"bon",
|
||||
"bytes",
|
||||
"jacquard-common",
|
||||
"jacquard-derive",
|
||||
"jacquard-lexicon",
|
||||
"miette",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_ipld_dagcbor",
|
||||
"thiserror 2.0.17",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jacquard-axum"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed99b0dc0cd54189bebb83d5d5cc5ac2889f62ede9729a6ead9035073d111bc9"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bytes",
|
||||
"jacquard",
|
||||
"jacquard-common",
|
||||
"jacquard-derive",
|
||||
"jacquard-identity",
|
||||
"miette",
|
||||
"multibase",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jacquard-common"
|
||||
version = "0.9.5"
|
||||
@@ -3256,34 +3118,6 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jacquard-identity"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7aaefa819fa4213cf59f180dba932f018a7cd0599582fd38474ee2a38c16cf2"
|
||||
dependencies = [
|
||||
"bon",
|
||||
"bytes",
|
||||
"hickory-resolver",
|
||||
"http 1.4.0",
|
||||
"jacquard-api",
|
||||
"jacquard-common",
|
||||
"jacquard-lexicon",
|
||||
"miette",
|
||||
"mini-moka-wasm",
|
||||
"n0-future",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"trait-variant",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jacquard-lexicon"
|
||||
version = "0.9.5"
|
||||
@@ -3311,36 +3145,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jacquard-oauth"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68bf0b0e061d85b09cfa78588dc098918d5b62f539a719165c6a806a1d2c0ef2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"elliptic-curve 0.13.8",
|
||||
"http 1.4.0",
|
||||
"jacquard-common",
|
||||
"jacquard-identity",
|
||||
"jose-jwa",
|
||||
"jose-jwk",
|
||||
"miette",
|
||||
"p256 0.13.2",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"smol_str",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"trait-variant",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jacquard-repo"
|
||||
version = "0.9.6"
|
||||
@@ -3378,42 +3182,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jose-b64"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"serde",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jose-jwa"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jose-jwk"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7"
|
||||
dependencies = [
|
||||
"jose-b64",
|
||||
"jose-jwa",
|
||||
"p256 0.13.2",
|
||||
"p384",
|
||||
"rsa",
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.83"
|
||||
@@ -3424,29 +3192,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "10.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
"getrandom 0.2.16",
|
||||
"hmac",
|
||||
"js-sys",
|
||||
"p256 0.13.2",
|
||||
"p384",
|
||||
"pem",
|
||||
"rand 0.8.5",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"signature 2.2.0",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.4"
|
||||
@@ -3593,38 +3338,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
|
||||
dependencies = [
|
||||
"log",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever_rcdom"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
|
||||
dependencies = [
|
||||
"html5ever",
|
||||
"markup5ever",
|
||||
"tendril",
|
||||
"xml5ever",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match-lookup"
|
||||
version = "0.1.1"
|
||||
@@ -3751,21 +3464,6 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mini-moka-wasm"
|
||||
version = "0.10.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0102b9a2ad50fa47ca89eead2316c8222285ecfbd3f69ce99564fbe4253866e8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-utils",
|
||||
"dashmap",
|
||||
"smallvec",
|
||||
"tagptr",
|
||||
"triomphe",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -3864,12 +3562,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -4191,16 +3883,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -4216,44 +3898,6 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
@@ -4391,12 +4035,6 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -5391,24 +5029,6 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "sketches-ddsketch"
|
||||
version = "0.3.0"
|
||||
@@ -5734,31 +5354,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
|
||||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"parking_lot",
|
||||
"phf_shared",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache_codegen"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
@@ -5868,12 +5463,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.23.0"
|
||||
@@ -5887,17 +5476,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendril"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
|
||||
dependencies = [
|
||||
"futf",
|
||||
"mac",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "testcontainers"
|
||||
version = "0.26.2"
|
||||
@@ -6343,7 +5921,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"thiserror 2.0.17",
|
||||
"totp-rs",
|
||||
"tranquil-crypto",
|
||||
"urlencoding",
|
||||
@@ -6357,7 +5934,6 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"redis",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"tranquil-infra",
|
||||
]
|
||||
@@ -6368,15 +5944,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tranquil-db-traits",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -6391,7 +5963,6 @@ dependencies = [
|
||||
"p256 0.13.2",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"thiserror 2.0.17",
|
||||
@@ -6407,7 +5978,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-oauth",
|
||||
@@ -6439,8 +6009,6 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6472,7 +6040,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
"aws-sdk-s3",
|
||||
"axum",
|
||||
@@ -6487,11 +6054,9 @@ dependencies = [
|
||||
"cid",
|
||||
"ctor",
|
||||
"dotenvy",
|
||||
"ed25519-dalek",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"governor",
|
||||
"hex",
|
||||
"hickory-resolver",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
@@ -6500,17 +6065,14 @@ dependencies = [
|
||||
"infer",
|
||||
"ipld-core",
|
||||
"iroh-car",
|
||||
"jacquard",
|
||||
"jacquard-axum",
|
||||
"jacquard-common",
|
||||
"jacquard-repo",
|
||||
"jsonwebtoken",
|
||||
"k256",
|
||||
"metrics",
|
||||
"metrics-exporter-prometheus",
|
||||
"multibase",
|
||||
"multihash",
|
||||
"p256 0.13.2",
|
||||
"p384",
|
||||
"rand 0.8.5",
|
||||
"redis",
|
||||
"regex",
|
||||
@@ -6528,7 +6090,6 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"totp-rs",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-layer",
|
||||
@@ -6540,7 +6101,6 @@ dependencies = [
|
||||
"tranquil-crypto",
|
||||
"tranquil-db",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-infra",
|
||||
"tranquil-oauth",
|
||||
"tranquil-repo",
|
||||
"tranquil-scopes",
|
||||
@@ -6549,7 +6109,6 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"webauthn-rs",
|
||||
"webauthn-rs-proto",
|
||||
"wiremock",
|
||||
"zip",
|
||||
]
|
||||
@@ -6564,7 +6123,6 @@ dependencies = [
|
||||
"multihash",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"tranquil-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6590,8 +6148,6 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"sha2",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"tranquil-infra",
|
||||
]
|
||||
|
||||
@@ -6601,19 +6157,13 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cid",
|
||||
"jacquard",
|
||||
"jacquard-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -6995,18 +6545,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpage"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac"
|
||||
dependencies = [
|
||||
"html5ever",
|
||||
"markup5ever_rcdom",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -7433,17 +6971,6 @@ dependencies = [
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml5ever"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.6"
|
||||
@@ -7531,9 +7058,6 @@ name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
||||
+1
-2
@@ -63,8 +63,7 @@ image = { version = "0.25", default-features = false, features = ["jpeg", "png",
|
||||
infer = "0.19"
|
||||
ipld-core = "0.4"
|
||||
iroh-car = "0.5"
|
||||
jacquard = { version = "0.9", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] }
|
||||
jacquard-axum = "0.9"
|
||||
jacquard-common = { version = "0.9", features = ["crypto-k256"] }
|
||||
jacquard-repo = "0.9"
|
||||
jsonwebtoken = { version = "10.2", features = ["rust_crypto"] }
|
||||
k256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] }
|
||||
|
||||
@@ -19,7 +19,6 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
totp-rs = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -10,5 +10,4 @@ tranquil-infra = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -7,13 +7,9 @@ license.workspace = true
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tranquil-db-traits = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -12,7 +12,6 @@ hmac = { workspace = true }
|
||||
p256 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -53,10 +53,7 @@ pub struct BlobExportInfo {
|
||||
|
||||
#[async_trait]
|
||||
pub trait BackupRepository: Send + Sync {
|
||||
async fn get_user_backup_status(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<(Uuid, bool)>, DbError>;
|
||||
async fn get_user_backup_status(&self, did: &Did) -> Result<Option<(Uuid, bool)>, DbError>;
|
||||
|
||||
async fn list_backups_for_user(&self, user_id: Uuid) -> Result<Vec<BackupRow>, DbError>;
|
||||
|
||||
@@ -92,8 +89,10 @@ pub trait BackupRepository: Send + Sync {
|
||||
did: &Did,
|
||||
) -> Result<Option<BackupForDeletion>, DbError>;
|
||||
|
||||
async fn get_user_deactivated_status(&self, did: &Did)
|
||||
-> Result<Option<Option<DateTime<Utc>>>, DbError>;
|
||||
async fn get_user_deactivated_status(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<Option<DateTime<Utc>>>, DbError>;
|
||||
|
||||
async fn update_backup_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError>;
|
||||
|
||||
|
||||
@@ -58,11 +58,7 @@ pub trait BlobRepository: Send + Sync {
|
||||
limit: i64,
|
||||
) -> Result<Vec<CidLink>, DbError>;
|
||||
|
||||
async fn list_blobs_since_rev(
|
||||
&self,
|
||||
did: &Did,
|
||||
since: &str,
|
||||
) -> Result<Vec<CidLink>, DbError>;
|
||||
async fn list_blobs_since_rev(&self, did: &Did, since: &str) -> Result<Vec<CidLink>, DbError>;
|
||||
|
||||
async fn count_blobs_by_user(&self, user_id: Uuid) -> Result<i64, DbError>;
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ pub trait DelegationRepository: Send + Sync {
|
||||
|
||||
async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn log_delegation_action(
|
||||
&self,
|
||||
delegated_did: &Did,
|
||||
|
||||
@@ -109,6 +109,7 @@ pub struct DeletionRequest {
|
||||
|
||||
#[async_trait]
|
||||
pub trait InfraRepository: Send + Sync {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn enqueue_comms(
|
||||
&self,
|
||||
user_id: Option<Uuid>,
|
||||
@@ -287,10 +288,7 @@ pub trait InfraRepository: Send + Sync {
|
||||
limit: i64,
|
||||
) -> Result<Vec<NotificationHistoryRow>, DbError>;
|
||||
|
||||
async fn get_server_configs(
|
||||
&self,
|
||||
keys: &[&str],
|
||||
) -> Result<Vec<(String, String)>, DbError>;
|
||||
async fn get_server_configs(&self, keys: &[&str]) -> Result<Vec<(String, String)>, DbError>;
|
||||
|
||||
async fn upsert_server_config(&self, key: &str, value: &str) -> Result<(), DbError>;
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ pub use backup::{
|
||||
BackupForDeletion, BackupRepository, BackupRow, BackupStorageInfo, BlobExportInfo,
|
||||
OldBackupInfo, UserBackupInfo,
|
||||
};
|
||||
pub use blob::{
|
||||
BlobForExport, BlobMetadata, BlobRepository, BlobWithTakedown, MissingBlobInfo,
|
||||
};
|
||||
pub use blob::{BlobForExport, BlobMetadata, BlobRepository, BlobWithTakedown, MissingBlobInfo};
|
||||
pub use delegation::{
|
||||
AuditLogEntry, ControllerInfo, DelegatedAccountInfo, DelegationActionType, DelegationGrant,
|
||||
DelegationRepository,
|
||||
@@ -43,16 +41,17 @@ pub use session::{
|
||||
SessionMfaStatus, SessionRefreshData, SessionRepository, SessionToken, SessionTokenCreate,
|
||||
};
|
||||
pub use user::{
|
||||
AccountSearchResult, CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput,
|
||||
CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult,
|
||||
DidWebOverrides, MigrationReactivationError, MigrationReactivationInput, NotificationPrefs,
|
||||
OAuthTokenWithUser, PasswordResetResult, ReactivatedAccountInfo, RecoverPasskeyAccountInput,
|
||||
RecoverPasskeyAccountResult, ScheduledDeletionAccount, StoredBackupCode, StoredPasskey,
|
||||
TotpRecord, User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo,
|
||||
UserEmailInfo, UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery,
|
||||
AccountSearchResult, CompletePasskeySetupInput, CreateAccountError,
|
||||
CreateDelegatedAccountInput, CreatePasskeyAccountInput, CreatePasswordAccountInput,
|
||||
CreatePasswordAccountResult, DidWebOverrides, MigrationReactivationError,
|
||||
MigrationReactivationInput, NotificationPrefs, OAuthTokenWithUser, PasswordResetResult,
|
||||
ReactivatedAccountInfo, RecoverPasskeyAccountInput, RecoverPasskeyAccountResult,
|
||||
ScheduledDeletionAccount, StoredBackupCode, StoredPasskey, TotpRecord, User2faStatus,
|
||||
UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo,
|
||||
UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery,
|
||||
UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle,
|
||||
UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId,
|
||||
UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo,
|
||||
UserRepository, UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo, UserStatus,
|
||||
UserVerificationInfo, UserWithKey,
|
||||
UserRepository, UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo,
|
||||
UserStatus, UserVerificationInfo, UserWithKey,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,10 @@ use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tranquil_oauth::{AuthorizedClientData, DeviceData, RequestData, TokenData};
|
||||
use tranquil_types::{AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, TokenId};
|
||||
use tranquil_types::{
|
||||
AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId,
|
||||
TokenId,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::DbError;
|
||||
@@ -106,7 +109,10 @@ pub trait OAuthRepository: Send + Sync {
|
||||
new_refresh_token: &RefreshToken,
|
||||
new_expires_at: DateTime<Utc>,
|
||||
) -> Result<(), DbError>;
|
||||
async fn check_refresh_token_used(&self, refresh_token: &RefreshToken) -> Result<Option<i32>, DbError>;
|
||||
async fn check_refresh_token_used(
|
||||
&self,
|
||||
refresh_token: &RefreshToken,
|
||||
) -> Result<Option<i32>, DbError>;
|
||||
async fn delete_token(&self, token_id: &TokenId) -> Result<(), DbError>;
|
||||
async fn delete_token_family(&self, db_id: i32) -> Result<(), DbError>;
|
||||
async fn list_tokens_for_user(&self, did: &Did) -> Result<Vec<TokenData>, DbError>;
|
||||
@@ -116,7 +122,11 @@ pub trait OAuthRepository: Send + Sync {
|
||||
did: &Did,
|
||||
keep_count: i64,
|
||||
) -> Result<u64, DbError>;
|
||||
async fn revoke_tokens_for_client(&self, did: &Did, client_id: &ClientId) -> Result<u64, DbError>;
|
||||
async fn revoke_tokens_for_client(
|
||||
&self,
|
||||
did: &Did,
|
||||
client_id: &ClientId,
|
||||
) -> Result<u64, DbError>;
|
||||
async fn revoke_tokens_for_controller(
|
||||
&self,
|
||||
delegated_did: &Did,
|
||||
@@ -157,9 +167,16 @@ pub trait OAuthRepository: Send + Sync {
|
||||
did: &Did,
|
||||
device_id: Option<&DeviceId>,
|
||||
) -> Result<(), DbError>;
|
||||
async fn update_request_scope(&self, request_id: &RequestId, scope: &str) -> Result<(), DbError>;
|
||||
async fn set_controller_did(&self, request_id: &RequestId, controller_did: &Did)
|
||||
-> Result<(), DbError>;
|
||||
async fn update_request_scope(
|
||||
&self,
|
||||
request_id: &RequestId,
|
||||
scope: &str,
|
||||
) -> Result<(), DbError>;
|
||||
async fn set_controller_did(
|
||||
&self,
|
||||
request_id: &RequestId,
|
||||
controller_did: &Did,
|
||||
) -> Result<(), DbError>;
|
||||
async fn set_request_did(&self, request_id: &RequestId, did: &Did) -> Result<(), DbError>;
|
||||
|
||||
async fn create_device(&self, device_id: &DeviceId, data: &DeviceData) -> Result<(), DbError>;
|
||||
@@ -167,8 +184,15 @@ pub trait OAuthRepository: Send + Sync {
|
||||
async fn update_device_last_seen(&self, device_id: &DeviceId) -> Result<(), DbError>;
|
||||
async fn delete_device(&self, device_id: &DeviceId) -> Result<(), DbError>;
|
||||
async fn upsert_account_device(&self, did: &Did, device_id: &DeviceId) -> Result<(), DbError>;
|
||||
async fn get_device_accounts(&self, device_id: &DeviceId) -> Result<Vec<DeviceAccountRow>, DbError>;
|
||||
async fn verify_account_on_device(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError>;
|
||||
async fn get_device_accounts(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
) -> Result<Vec<DeviceAccountRow>, DbError>;
|
||||
async fn verify_account_on_device(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
) -> Result<bool, DbError>;
|
||||
|
||||
async fn check_and_record_dpop_jti(&self, jti: &DPoPProofId) -> Result<bool, DbError>;
|
||||
async fn cleanup_expired_dpop_jtis(&self, max_age_secs: i64) -> Result<u64, DbError>;
|
||||
@@ -184,7 +208,10 @@ pub trait OAuthRepository: Send + Sync {
|
||||
) -> Result<Option<TwoFactorChallenge>, DbError>;
|
||||
async fn increment_2fa_attempts(&self, id: Uuid) -> Result<i32, DbError>;
|
||||
async fn delete_2fa_challenge(&self, id: Uuid) -> Result<(), DbError>;
|
||||
async fn delete_2fa_challenge_by_request_uri(&self, request_uri: &RequestId) -> Result<(), DbError>;
|
||||
async fn delete_2fa_challenge_by_request_uri(
|
||||
&self,
|
||||
request_uri: &RequestId,
|
||||
) -> Result<(), DbError>;
|
||||
async fn cleanup_expired_2fa_challenges(&self) -> Result<u64, DbError>;
|
||||
async fn check_user_2fa_enabled(&self, did: &Did) -> Result<bool, DbError>;
|
||||
|
||||
@@ -199,7 +226,11 @@ pub trait OAuthRepository: Send + Sync {
|
||||
client_id: &ClientId,
|
||||
prefs: &[ScopePreference],
|
||||
) -> Result<(), DbError>;
|
||||
async fn delete_scope_preferences(&self, did: &Did, client_id: &ClientId) -> Result<(), DbError>;
|
||||
async fn delete_scope_preferences(
|
||||
&self,
|
||||
did: &Did,
|
||||
client_id: &ClientId,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn upsert_authorized_client(
|
||||
&self,
|
||||
@@ -219,7 +250,11 @@ pub trait OAuthRepository: Send + Sync {
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
) -> Result<Option<DeviceTrustInfo>, DbError>;
|
||||
async fn device_belongs_to_user(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError>;
|
||||
async fn device_belongs_to_user(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
) -> Result<bool, DbError>;
|
||||
async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError>;
|
||||
async fn update_device_friendly_name(
|
||||
&self,
|
||||
@@ -241,5 +276,9 @@ pub trait OAuthRepository: Send + Sync {
|
||||
async fn list_sessions_by_did(&self, did: &Did) -> Result<Vec<OAuthSessionListItem>, DbError>;
|
||||
async fn delete_session_by_id(&self, session_id: i32, did: &Did) -> Result<u64, DbError>;
|
||||
async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError>;
|
||||
async fn delete_sessions_by_did_except(&self, did: &Did, except_token_id: &TokenId) -> Result<u64, DbError>;
|
||||
async fn delete_sessions_by_did_except(
|
||||
&self,
|
||||
did: &Did,
|
||||
except_token_id: &TokenId,
|
||||
) -> Result<u64, DbError>;
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@ pub trait RepoRepository: Send + Sync {
|
||||
rkey: &Rkey,
|
||||
) -> Result<Option<CidLink>, DbError>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn list_records(
|
||||
&self,
|
||||
repo_id: Uuid,
|
||||
@@ -252,10 +253,14 @@ pub trait RepoRepository: Send + Sync {
|
||||
|
||||
async fn count_all_records(&self) -> Result<i64, DbError>;
|
||||
|
||||
async fn get_record_by_cid(&self, cid: &CidLink) -> Result<Option<RecordWithTakedown>, DbError>;
|
||||
async fn get_record_by_cid(&self, cid: &CidLink)
|
||||
-> Result<Option<RecordWithTakedown>, DbError>;
|
||||
|
||||
async fn set_record_takedown(&self, cid: &CidLink, takedown_ref: Option<&str>)
|
||||
-> Result<(), DbError>;
|
||||
async fn set_record_takedown(
|
||||
&self,
|
||||
cid: &CidLink,
|
||||
takedown_ref: Option<&str>,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn insert_user_blocks(
|
||||
&self,
|
||||
@@ -264,8 +269,11 @@ pub trait RepoRepository: Send + Sync {
|
||||
repo_rev: &str,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn delete_user_blocks(&self, user_id: Uuid, block_cids: &[Vec<u8>])
|
||||
-> Result<(), DbError>;
|
||||
async fn delete_user_blocks(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
block_cids: &[Vec<u8>],
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn get_user_block_cids_since_rev(
|
||||
&self,
|
||||
@@ -277,7 +285,11 @@ pub trait RepoRepository: Send + Sync {
|
||||
|
||||
async fn insert_commit_event(&self, data: &CommitEventData) -> Result<i64, DbError>;
|
||||
|
||||
async fn insert_identity_event(&self, did: &Did, handle: Option<&Handle>) -> Result<i64, DbError>;
|
||||
async fn insert_identity_event(
|
||||
&self,
|
||||
did: &Did,
|
||||
handle: Option<&Handle>,
|
||||
) -> Result<i64, DbError>;
|
||||
|
||||
async fn insert_account_event(
|
||||
&self,
|
||||
@@ -302,7 +314,7 @@ pub trait RepoRepository: Send + Sync {
|
||||
) -> Result<i64, DbError>;
|
||||
|
||||
async fn update_seq_blocks_cids(&self, seq: i64, blocks_cids: &[String])
|
||||
-> Result<(), DbError>;
|
||||
-> Result<(), DbError>;
|
||||
|
||||
async fn delete_sequences_except(&self, did: &Did, keep_seq: i64) -> Result<(), DbError>;
|
||||
|
||||
@@ -344,7 +356,8 @@ pub trait RepoRepository: Send + Sync {
|
||||
limit: i64,
|
||||
) -> Result<Vec<RepoListItem>, DbError>;
|
||||
|
||||
async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError>;
|
||||
async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid)
|
||||
-> Result<Option<CidLink>, DbError>;
|
||||
|
||||
async fn notify_update(&self, seq: i64) -> Result<(), DbError>;
|
||||
|
||||
|
||||
@@ -139,32 +139,34 @@ pub trait UserRepository: Send + Sync {
|
||||
|
||||
async fn get_user_key_by_id(&self, user_id: Uuid) -> Result<Option<UserKeyInfo>, DbError>;
|
||||
|
||||
async fn get_id_and_handle_by_did(&self, did: &Did) -> Result<Option<UserIdAndHandle>, DbError>;
|
||||
async fn get_id_and_handle_by_did(&self, did: &Did)
|
||||
-> Result<Option<UserIdAndHandle>, DbError>;
|
||||
|
||||
async fn get_did_web_info_by_handle(
|
||||
&self,
|
||||
handle: &Handle,
|
||||
) -> Result<Option<UserDidWebInfo>, DbError>;
|
||||
|
||||
async fn get_did_web_overrides(&self, user_id: Uuid) -> Result<Option<DidWebOverrides>, DbError>;
|
||||
async fn get_did_web_overrides(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<DidWebOverrides>, DbError>;
|
||||
|
||||
async fn get_handle_by_did(&self, did: &Did) -> Result<Option<Handle>, DbError>;
|
||||
|
||||
async fn is_account_active_by_did(&self, did: &Did) -> Result<Option<bool>, DbError>;
|
||||
|
||||
async fn get_user_for_deletion(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserForDeletion>, DbError>;
|
||||
async fn get_user_for_deletion(&self, did: &Did) -> Result<Option<UserForDeletion>, DbError>;
|
||||
|
||||
async fn check_handle_exists(&self, handle: &Handle, exclude_user_id: Uuid) -> Result<bool, DbError>;
|
||||
async fn check_handle_exists(
|
||||
&self,
|
||||
handle: &Handle,
|
||||
exclude_user_id: Uuid,
|
||||
) -> Result<bool, DbError>;
|
||||
|
||||
async fn update_handle(&self, user_id: Uuid, handle: &Handle) -> Result<(), DbError>;
|
||||
|
||||
async fn get_user_with_key_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserKeyWithId>, DbError>;
|
||||
async fn get_user_with_key_by_did(&self, did: &Did) -> Result<Option<UserKeyWithId>, DbError>;
|
||||
|
||||
async fn is_account_migrated(&self, did: &Did) -> Result<bool, DbError>;
|
||||
|
||||
@@ -174,7 +176,8 @@ pub trait UserRepository: Send + Sync {
|
||||
|
||||
async fn get_email_info_by_did(&self, did: &Did) -> Result<Option<UserEmailInfo>, DbError>;
|
||||
|
||||
async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) -> Result<bool, DbError>;
|
||||
async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid)
|
||||
-> Result<bool, DbError>;
|
||||
|
||||
async fn update_email(&self, user_id: Uuid, email: &str) -> Result<(), DbError>;
|
||||
|
||||
@@ -191,14 +194,16 @@ pub trait UserRepository: Send + Sync {
|
||||
|
||||
async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError>;
|
||||
|
||||
async fn get_notification_prefs(&self, did: &Did) -> Result<Option<NotificationPrefs>, DbError>;
|
||||
async fn get_notification_prefs(&self, did: &Did)
|
||||
-> Result<Option<NotificationPrefs>, DbError>;
|
||||
|
||||
async fn get_id_handle_email_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserIdHandleEmail>, DbError>;
|
||||
|
||||
async fn update_preferred_comms_channel(&self, did: &Did, channel: &str) -> Result<(), DbError>;
|
||||
async fn update_preferred_comms_channel(&self, did: &Did, channel: &str)
|
||||
-> Result<(), DbError>;
|
||||
|
||||
async fn clear_discord(&self, user_id: Uuid) -> Result<(), DbError>;
|
||||
|
||||
@@ -221,8 +226,11 @@ pub trait UserRepository: Send + Sync {
|
||||
telegram_username: &str,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn verify_signal_channel(&self, user_id: Uuid, signal_number: &str)
|
||||
-> Result<(), DbError>;
|
||||
async fn verify_signal_channel(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
signal_number: &str,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn set_email_verified_flag(&self, user_id: Uuid) -> Result<(), DbError>;
|
||||
|
||||
@@ -276,8 +284,11 @@ pub trait UserRepository: Send + Sync {
|
||||
challenge_type: &str,
|
||||
) -> Result<Option<String>, DbError>;
|
||||
|
||||
async fn delete_webauthn_challenge(&self, did: &Did, challenge_type: &str)
|
||||
-> Result<(), DbError>;
|
||||
async fn delete_webauthn_challenge(
|
||||
&self,
|
||||
did: &Did,
|
||||
challenge_type: &str,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn get_totp_record(&self, did: &Did) -> Result<Option<TotpRecord>, DbError>;
|
||||
|
||||
@@ -316,7 +327,10 @@ pub trait UserRepository: Send + Sync {
|
||||
|
||||
async fn get_session_info_by_did(&self, did: &Did) -> Result<Option<UserSessionInfo>, DbError>;
|
||||
|
||||
async fn get_legacy_login_pref(&self, did: &Did) -> Result<Option<UserLegacyLoginPref>, DbError>;
|
||||
async fn get_legacy_login_pref(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserLegacyLoginPref>, DbError>;
|
||||
|
||||
async fn update_legacy_login(&self, did: &Did, allow: bool) -> Result<bool, DbError>;
|
||||
|
||||
@@ -364,7 +378,8 @@ pub trait UserRepository: Send + Sync {
|
||||
did: &Did,
|
||||
) -> Result<Option<UserIdAndPasswordHash>, DbError>;
|
||||
|
||||
async fn update_password_hash(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError>;
|
||||
async fn update_password_hash(&self, user_id: Uuid, password_hash: &str)
|
||||
-> Result<(), DbError>;
|
||||
|
||||
async fn reset_password_with_sessions(
|
||||
&self,
|
||||
@@ -389,23 +404,30 @@ pub trait UserRepository: Send + Sync {
|
||||
|
||||
async fn remove_user_password(&self, user_id: Uuid) -> Result<(), DbError>;
|
||||
|
||||
async fn set_new_user_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError>;
|
||||
async fn set_new_user_password(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
password_hash: &str,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn get_user_key_by_did(&self, did: &Did) -> Result<Option<UserKeyInfo>, DbError>;
|
||||
|
||||
async fn delete_account_complete(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
did: &Did,
|
||||
) -> Result<(), DbError>;
|
||||
async fn delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError>;
|
||||
|
||||
async fn set_user_takedown(&self, did: &Did, takedown_ref: Option<&str>) -> Result<bool, DbError>;
|
||||
async fn set_user_takedown(
|
||||
&self,
|
||||
did: &Did,
|
||||
takedown_ref: Option<&str>,
|
||||
) -> Result<bool, DbError>;
|
||||
|
||||
async fn admin_delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError>;
|
||||
|
||||
async fn get_user_for_did_doc(&self, did: &Did) -> Result<Option<UserForDidDoc>, DbError>;
|
||||
|
||||
async fn get_user_for_did_doc_build(&self, did: &Did) -> Result<Option<UserForDidDocBuild>, DbError>;
|
||||
async fn get_user_for_did_doc_build(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserForDidDocBuild>, DbError>;
|
||||
|
||||
async fn upsert_did_web_overrides(
|
||||
&self,
|
||||
@@ -414,13 +436,12 @@ pub trait UserRepository: Send + Sync {
|
||||
also_known_as: Option<Vec<String>>,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn update_migrated_to_pds(
|
||||
async fn update_migrated_to_pds(&self, did: &Did, endpoint: &str) -> Result<(), DbError>;
|
||||
|
||||
async fn get_user_for_passkey_setup(
|
||||
&self,
|
||||
did: &Did,
|
||||
endpoint: &str,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn get_user_for_passkey_setup(&self, did: &Did) -> Result<Option<UserForPasskeySetup>, DbError>;
|
||||
) -> Result<Option<UserForPasskeySetup>, DbError>;
|
||||
|
||||
async fn get_user_for_passkey_recovery(
|
||||
&self,
|
||||
@@ -442,11 +463,7 @@ pub trait UserRepository: Send + Sync {
|
||||
limit: i64,
|
||||
) -> Result<Vec<ScheduledDeletionAccount>, DbError>;
|
||||
|
||||
async fn delete_account_with_firehose(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
did: &Did,
|
||||
) -> Result<i64, DbError>;
|
||||
async fn delete_account_with_firehose(&self, user_id: Uuid, did: &Did) -> Result<i64, DbError>;
|
||||
|
||||
async fn create_password_account(
|
||||
&self,
|
||||
@@ -468,7 +485,10 @@ pub trait UserRepository: Send + Sync {
|
||||
input: &MigrationReactivationInput,
|
||||
) -> Result<ReactivatedAccountInfo, MigrationReactivationError>;
|
||||
|
||||
async fn check_handle_available_for_new_account(&self, handle: &Handle) -> Result<bool, DbError>;
|
||||
async fn check_handle_available_for_new_account(
|
||||
&self,
|
||||
handle: &Handle,
|
||||
) -> Result<bool, DbError>;
|
||||
|
||||
async fn check_and_consume_invite_code(&self, code: &str) -> Result<bool, DbError>;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ chrono = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
|
||||
@@ -80,13 +80,11 @@ impl BlobRepository for PostgresBlobRepository {
|
||||
}
|
||||
|
||||
async fn get_blob_storage_key(&self, cid: &CidLink) -> Result<Option<String>, DbError> {
|
||||
let result = sqlx::query_scalar!(
|
||||
"SELECT storage_key FROM blobs WHERE cid = $1",
|
||||
cid.as_str()
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let result =
|
||||
sqlx::query_scalar!("SELECT storage_key FROM blobs WHERE cid = $1", cid.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -114,11 +112,7 @@ impl BlobRepository for PostgresBlobRepository {
|
||||
Ok(results.into_iter().map(CidLink::from).collect())
|
||||
}
|
||||
|
||||
async fn list_blobs_since_rev(
|
||||
&self,
|
||||
did: &Did,
|
||||
since: &str,
|
||||
) -> Result<Vec<CidLink>, DbError> {
|
||||
async fn list_blobs_since_rev(&self, did: &Did, since: &str) -> Result<Vec<CidLink>, DbError> {
|
||||
let results = sqlx::query_scalar!(
|
||||
r#"SELECT DISTINCT unnest(blobs) as "cid!"
|
||||
FROM repo_seq
|
||||
|
||||
@@ -62,15 +62,18 @@ impl PostgresDelegationRepository {
|
||||
#[async_trait]
|
||||
impl DelegationRepository for PostgresDelegationRepository {
|
||||
async fn is_delegated_account(&self, did: &Did) -> Result<bool, DbError> {
|
||||
let result = sqlx::query_scalar!(
|
||||
r#"SELECT account_type::text = 'delegated' as "is_delegated!" FROM users WHERE did = $1"#,
|
||||
let exists = sqlx::query_scalar!(
|
||||
r#"SELECT EXISTS(
|
||||
SELECT 1 FROM account_delegations
|
||||
WHERE delegated_did = $1 AND revoked_at IS NULL
|
||||
) as "exists!""#,
|
||||
did.as_str()
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result.unwrap_or(false))
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn create_delegation(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::postgres::PgListener;
|
||||
use sqlx::PgPool;
|
||||
use sqlx::postgres::PgListener;
|
||||
use tranquil_db_traits::{DbError, RepoEventNotifier, RepoEventReceiver};
|
||||
|
||||
use super::user::map_sqlx_error;
|
||||
@@ -21,7 +21,10 @@ impl RepoEventNotifier for PostgresRepoEventNotifier {
|
||||
let mut listener = PgListener::connect_with(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
listener.listen("repo_updates").await.map_err(map_sqlx_error)?;
|
||||
listener
|
||||
.listen("repo_updates")
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
Ok(Box::new(PostgresRepoEventReceiver { listener }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,77 +310,69 @@ impl InfraRepository for PostgresInfraRepository {
|
||||
sort: InviteCodeSortOrder,
|
||||
) -> Result<Vec<InviteCodeRow>, DbError> {
|
||||
let results = match (cursor, sort) {
|
||||
(Some(cursor_code), InviteCodeSortOrder::Recent) => {
|
||||
sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
(Some(cursor_code), InviteCodeSortOrder::Recent) => sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
FROM invite_codes ic
|
||||
WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2"#,
|
||||
cursor_code,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?
|
||||
}
|
||||
(None, InviteCodeSortOrder::Recent) => {
|
||||
sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
cursor_code,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?,
|
||||
(None, InviteCodeSortOrder::Recent) => sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
FROM invite_codes ic
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1"#,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?
|
||||
}
|
||||
(Some(cursor_code), InviteCodeSortOrder::Usage) => {
|
||||
sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?,
|
||||
(Some(cursor_code), InviteCodeSortOrder::Usage) => sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
FROM invite_codes ic
|
||||
WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1)
|
||||
ORDER BY available_uses DESC
|
||||
LIMIT $2"#,
|
||||
cursor_code,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?
|
||||
}
|
||||
(None, InviteCodeSortOrder::Usage) => {
|
||||
sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
cursor_code,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?,
|
||||
(None, InviteCodeSortOrder::Usage) => sqlx::query_as!(
|
||||
InviteCodeRow,
|
||||
r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
|
||||
FROM invite_codes ic
|
||||
ORDER BY available_uses DESC
|
||||
LIMIT $1"#,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?
|
||||
}
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?,
|
||||
};
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn get_user_dids_by_ids(&self, user_ids: &[Uuid]) -> Result<Vec<(Uuid, Did)>, DbError> {
|
||||
let results = sqlx::query!(
|
||||
"SELECT id, did FROM users WHERE id = ANY($1)",
|
||||
user_ids
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let results = sqlx::query!("SELECT id, did FROM users WHERE id = ANY($1)", user_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(results.into_iter().map(|r| (r.id, Did::from(r.did))).collect())
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| (r.id, Did::from(r.did)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_invite_code_uses_batch(
|
||||
@@ -688,11 +680,10 @@ impl InfraRepository for PostgresInfraRepository {
|
||||
}
|
||||
|
||||
async fn get_server_config(&self, key: &str) -> Result<Option<String>, DbError> {
|
||||
let row =
|
||||
sqlx::query_scalar!("SELECT value FROM server_config WHERE key = $1", key)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let row = sqlx::query_scalar!("SELECT value FROM server_config WHERE key = $1", key)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
@@ -881,13 +872,12 @@ impl InfraRepository for PostgresInfraRepository {
|
||||
|
||||
async fn get_server_configs(&self, keys: &[&str]) -> Result<Vec<(String, String)>, DbError> {
|
||||
let keys_vec: Vec<String> = keys.iter().map(|s| s.to_string()).collect();
|
||||
let rows: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT key, value FROM server_config WHERE key = ANY($1)",
|
||||
)
|
||||
.bind(&keys_vec)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let rows: Vec<(String, String)> =
|
||||
sqlx::query_as("SELECT key, value FROM server_config WHERE key = ANY($1)")
|
||||
.bind(&keys_vec)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -917,10 +907,11 @@ impl InfraRepository for PostgresInfraRepository {
|
||||
}
|
||||
|
||||
async fn get_blob_storage_key_by_cid(&self, cid: &CidLink) -> Result<Option<String>, DbError> {
|
||||
let result = sqlx::query_scalar!("SELECT storage_key FROM blobs WHERE cid = $1", cid.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let result =
|
||||
sqlx::query_scalar!("SELECT storage_key FROM blobs WHERE cid = $1", cid.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ pub use infra::PostgresInfraRepository;
|
||||
pub use oauth::PostgresOAuthRepository;
|
||||
pub use repo::PostgresRepoRepository;
|
||||
pub use session::PostgresSessionRepository;
|
||||
pub use user::PostgresUserRepository;
|
||||
use tranquil_db_traits::{
|
||||
BacklinkRepository, BackupRepository, BlobRepository, DelegationRepository, InfraRepository,
|
||||
OAuthRepository, RepoEventNotifier, RepoRepository, SessionRepository, UserRepository,
|
||||
};
|
||||
pub use user::PostgresUserRepository;
|
||||
|
||||
pub struct PostgresRepositories {
|
||||
pub pool: PgPool,
|
||||
|
||||
@@ -6,8 +6,14 @@ use tranquil_db_traits::{
|
||||
DbError, DeviceAccountRow, DeviceTrustInfo, OAuthRepository, OAuthSessionListItem,
|
||||
ScopePreference, TrustedDeviceRow, TwoFactorChallenge,
|
||||
};
|
||||
use tranquil_oauth::{AuthorizedClientData, ClientAuth, AuthorizationRequestParameters, DeviceData, RequestData, TokenData};
|
||||
use tranquil_types::{AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, TokenId};
|
||||
use tranquil_oauth::{
|
||||
AuthorizationRequestParameters, AuthorizedClientData, ClientAuth, DeviceData, RequestData,
|
||||
TokenData,
|
||||
};
|
||||
use tranquil_types::{
|
||||
AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId,
|
||||
TokenId,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::user::map_sqlx_error;
|
||||
@@ -236,7 +242,10 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_refresh_token_used(&self, refresh_token: &RefreshToken) -> Result<Option<i32>, DbError> {
|
||||
async fn check_refresh_token_used(
|
||||
&self,
|
||||
refresh_token: &RefreshToken,
|
||||
) -> Result<Option<i32>, DbError> {
|
||||
let row = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT token_id FROM oauth_used_refresh_token WHERE refresh_token = $1
|
||||
@@ -347,7 +356,11 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn revoke_tokens_for_client(&self, did: &Did, client_id: &ClientId) -> Result<u64, DbError> {
|
||||
async fn revoke_tokens_for_client(
|
||||
&self,
|
||||
did: &Did,
|
||||
client_id: &ClientId,
|
||||
) -> Result<u64, DbError> {
|
||||
let result = sqlx::query!(
|
||||
"DELETE FROM oauth_token WHERE did = $1 AND client_id = $2",
|
||||
did.as_str(),
|
||||
@@ -574,7 +587,11 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_request_scope(&self, request_id: &RequestId, scope: &str) -> Result<(), DbError> {
|
||||
async fn update_request_scope(
|
||||
&self,
|
||||
request_id: &RequestId,
|
||||
scope: &str,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth_authorization_request
|
||||
@@ -708,7 +725,10 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_device_accounts(&self, device_id: &DeviceId) -> Result<Vec<DeviceAccountRow>, DbError> {
|
||||
async fn get_device_accounts(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
) -> Result<Vec<DeviceAccountRow>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT u.did, u.handle, u.email, ad.updated_at as last_used_at
|
||||
@@ -735,7 +755,11 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn verify_account_on_device(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError> {
|
||||
async fn verify_account_on_device(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
) -> Result<bool, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT 1 as "exists!"
|
||||
@@ -875,7 +899,10 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_2fa_challenge_by_request_uri(&self, request_uri: &RequestId) -> Result<(), DbError> {
|
||||
async fn delete_2fa_challenge_by_request_uri(
|
||||
&self,
|
||||
request_uri: &RequestId,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_2fa_challenge WHERE request_uri = $1
|
||||
@@ -966,7 +993,11 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_scope_preferences(&self, did: &Did, client_id: &ClientId) -> Result<(), DbError> {
|
||||
async fn delete_scope_preferences(
|
||||
&self,
|
||||
did: &Did,
|
||||
client_id: &ClientId,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_scope_preference
|
||||
@@ -1074,7 +1105,11 @@ impl OAuthRepository for PostgresOAuthRepository {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn device_belongs_to_user(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError> {
|
||||
async fn device_belongs_to_user(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
) -> Result<bool, DbError> {
|
||||
let exists = sqlx::query_scalar!(
|
||||
r#"SELECT 1 as "one!" FROM oauth_device od
|
||||
JOIN oauth_account_device oad ON od.id = oad.device_id
|
||||
|
||||
@@ -147,22 +147,19 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
}
|
||||
|
||||
async fn count_repos(&self) -> Result<i64, DbError> {
|
||||
let count =
|
||||
sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!" FROM repos"#)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let count = sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!" FROM repos"#)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn get_repos_without_rev(&self) -> Result<Vec<RepoWithoutRev>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
"SELECT user_id, repo_root_cid FROM repos WHERE repo_rev IS NULL"
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let rows = sqlx::query!("SELECT user_id, repo_root_cid FROM repos WHERE repo_rev IS NULL")
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
@@ -522,7 +519,10 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn get_record_by_cid(&self, cid: &CidLink) -> Result<Option<RecordWithTakedown>, DbError> {
|
||||
async fn get_record_by_cid(
|
||||
&self,
|
||||
cid: &CidLink,
|
||||
) -> Result<Option<RecordWithTakedown>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT id, takedown_ref FROM records WHERE record_cid = $1",
|
||||
cid.as_str()
|
||||
@@ -651,7 +651,11 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
Ok(seq)
|
||||
}
|
||||
|
||||
async fn insert_identity_event(&self, did: &Did, handle: Option<&Handle>) -> Result<i64, DbError> {
|
||||
async fn insert_identity_event(
|
||||
&self,
|
||||
did: &Did,
|
||||
handle: Option<&Handle>,
|
||||
) -> Result<i64, DbError> {
|
||||
let handle_str = handle.map(|h| h.as_str());
|
||||
let seq = sqlx::query_scalar!(
|
||||
r#"
|
||||
@@ -1061,7 +1065,10 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError> {
|
||||
async fn get_repo_root_cid_by_user_id(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<CidLink>, DbError> {
|
||||
let cid = sqlx::query_scalar!(
|
||||
"SELECT repo_root_cid FROM repos WHERE user_id = $1",
|
||||
user_id
|
||||
@@ -1203,25 +1210,22 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
}
|
||||
}
|
||||
|
||||
let is_account_active: bool = sqlx::query_scalar(
|
||||
"SELECT deactivated_at IS NULL FROM users WHERE id = $1",
|
||||
)
|
||||
.bind(input.user_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| ApplyCommitError::Database(e.to_string()))?
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
let is_account_active: bool =
|
||||
sqlx::query_scalar("SELECT deactivated_at IS NULL FROM users WHERE id = $1")
|
||||
.bind(input.user_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| ApplyCommitError::Database(e.to_string()))?
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE repos SET repo_root_cid = $1, repo_rev = $2 WHERE user_id = $3",
|
||||
)
|
||||
.bind(&input.new_root_cid)
|
||||
.bind(&input.new_rev)
|
||||
.bind(input.user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| ApplyCommitError::Database(e.to_string()))?;
|
||||
sqlx::query("UPDATE repos SET repo_root_cid = $1, repo_rev = $2 WHERE user_id = $3")
|
||||
.bind(&input.new_root_cid)
|
||||
.bind(&input.new_rev)
|
||||
.bind(input.user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| ApplyCommitError::Database(e.to_string()))?;
|
||||
|
||||
if !input.new_block_cids.is_empty() {
|
||||
sqlx::query(
|
||||
@@ -1260,8 +1264,16 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
.iter()
|
||||
.map(|r| r.collection.as_str())
|
||||
.collect();
|
||||
let rkeys: Vec<&str> = input.record_upserts.iter().map(|r| r.rkey.as_str()).collect();
|
||||
let cids: Vec<&str> = input.record_upserts.iter().map(|r| r.cid.as_str()).collect();
|
||||
let rkeys: Vec<&str> = input
|
||||
.record_upserts
|
||||
.iter()
|
||||
.map(|r| r.rkey.as_str())
|
||||
.collect();
|
||||
let cids: Vec<&str> = input
|
||||
.record_upserts
|
||||
.iter()
|
||||
.map(|r| r.cid.as_str())
|
||||
.collect();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -1287,7 +1299,11 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
.iter()
|
||||
.map(|r| r.collection.as_str())
|
||||
.collect();
|
||||
let rkeys: Vec<&str> = input.record_deletes.iter().map(|r| r.rkey.as_str()).collect();
|
||||
let rkeys: Vec<&str> = input
|
||||
.record_deletes
|
||||
.iter()
|
||||
.map(|r| r.rkey.as_str())
|
||||
.collect();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -1366,9 +1382,7 @@ impl RepoRepository for PostgresRepoRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_users_without_blocks(
|
||||
&self,
|
||||
) -> Result<Vec<UserWithoutBlocks>, DbError> {
|
||||
async fn get_users_without_blocks(&self) -> Result<Vec<UserWithoutBlocks>, DbError> {
|
||||
let rows: Vec<(Uuid, String, Option<String>)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT u.id as user_id, r.repo_root_cid, r.repo_rev
|
||||
|
||||
@@ -8,11 +8,12 @@ use tranquil_db_traits::{
|
||||
AccountSearchResult, CommsChannel, DbError, DidWebOverrides, NotificationPrefs,
|
||||
OAuthTokenWithUser, PasswordResetResult, StoredBackupCode, StoredPasskey, TotpRecord,
|
||||
User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo,
|
||||
UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, UserForPasskeySetup,
|
||||
UserForRecovery, UserForVerification, UserIdAndHandle, UserIdAndPasswordHash, UserIdHandleEmail,
|
||||
UserInfoForAuth, UserKeyInfo, UserKeyWithId, UserLegacyLoginPref, UserLoginCheck, UserLoginFull,
|
||||
UserLoginInfo, UserPasswordInfo, UserRepository, UserResendVerification, UserResetCodeInfo,
|
||||
UserRow, UserSessionInfo, UserStatus, UserVerificationInfo, UserWithKey,
|
||||
UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery,
|
||||
UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle,
|
||||
UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId,
|
||||
UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo,
|
||||
UserRepository, UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo,
|
||||
UserStatus, UserVerificationInfo, UserWithKey,
|
||||
};
|
||||
|
||||
pub struct PostgresUserRepository {
|
||||
@@ -344,14 +345,14 @@ impl UserRepository for PostgresUserRepository {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_id_and_handle_by_did(&self, did: &Did) -> Result<Option<UserIdAndHandle>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT id, handle FROM users WHERE did = $1",
|
||||
did.as_str()
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
async fn get_id_and_handle_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserIdAndHandle>, DbError> {
|
||||
let row = sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
Ok(row.map(|r| UserIdAndHandle {
|
||||
id: r.id,
|
||||
handle: Handle::from(r.handle),
|
||||
@@ -376,7 +377,10 @@ impl UserRepository for PostgresUserRepository {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_did_web_overrides(&self, user_id: Uuid) -> Result<Option<DidWebOverrides>, DbError> {
|
||||
async fn get_did_web_overrides(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<DidWebOverrides>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1",
|
||||
user_id
|
||||
@@ -398,7 +402,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
Ok(handle.map(Handle::from))
|
||||
}
|
||||
|
||||
async fn check_handle_exists(&self, handle: &Handle, exclude_user_id: Uuid) -> Result<bool, DbError> {
|
||||
async fn check_handle_exists(
|
||||
&self,
|
||||
handle: &Handle,
|
||||
exclude_user_id: Uuid,
|
||||
) -> Result<bool, DbError> {
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM users WHERE handle = $1 AND id != $2) as \"exists!\"",
|
||||
handle.as_str(),
|
||||
@@ -422,10 +430,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_with_key_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserKeyWithId>, DbError> {
|
||||
async fn get_user_with_key_by_did(&self, did: &Did) -> Result<Option<UserKeyWithId>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT u.id, uk.key_bytes, uk.encryption_version
|
||||
FROM users u
|
||||
@@ -469,7 +474,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
Ok(row
|
||||
.map(|r| r.email_verified || r.discord_verified || r.telegram_verified || r.signal_verified)
|
||||
.map(|r| {
|
||||
r.email_verified || r.discord_verified || r.telegram_verified || r.signal_verified
|
||||
})
|
||||
.unwrap_or(false))
|
||||
}
|
||||
|
||||
@@ -497,7 +504,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) -> Result<bool, DbError> {
|
||||
async fn check_email_exists(
|
||||
&self,
|
||||
email: &str,
|
||||
exclude_user_id: Uuid,
|
||||
) -> Result<bool, DbError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
|
||||
email.to_lowercase(),
|
||||
@@ -583,7 +594,10 @@ impl UserRepository for PostgresUserRepository {
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn get_notification_prefs(&self, did: &Did) -> Result<Option<NotificationPrefs>, DbError> {
|
||||
async fn get_notification_prefs(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<NotificationPrefs>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT
|
||||
email,
|
||||
@@ -630,7 +644,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn update_preferred_comms_channel(&self, did: &Did, channel: &str) -> Result<(), DbError> {
|
||||
async fn update_preferred_comms_channel(
|
||||
&self,
|
||||
did: &Did,
|
||||
channel: &str,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET preferred_comms_channel = $1::comms_channel, updated_at = NOW() WHERE did = $2",
|
||||
)
|
||||
@@ -1349,10 +1367,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_session_info_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserSessionInfo>, DbError> {
|
||||
async fn get_session_info_by_did(&self, did: &Did) -> Result<Option<UserSessionInfo>, DbError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT handle, email, email_verified, is_admin, deactivated_at, takedown_ref,
|
||||
@@ -1647,7 +1662,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_password_hash(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError> {
|
||||
async fn update_password_hash(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
password_hash: &str,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"UPDATE users SET password_hash = $1 WHERE id = $2",
|
||||
password_hash,
|
||||
@@ -1769,7 +1788,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_new_user_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError> {
|
||||
async fn set_new_user_password(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
password_hash: &str,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"UPDATE users SET password_hash = $1, password_required = TRUE WHERE id = $2",
|
||||
password_hash,
|
||||
@@ -1854,10 +1877,13 @@ impl UserRepository for PostgresUserRepository {
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did.as_str())
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM account_deletion_requests WHERE did = $1",
|
||||
did.as_str()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
@@ -1866,7 +1892,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_user_takedown(&self, did: &Did, takedown_ref: Option<&str>) -> Result<bool, DbError> {
|
||||
async fn set_user_takedown(
|
||||
&self,
|
||||
did: &Did,
|
||||
takedown_ref: Option<&str>,
|
||||
) -> Result<bool, DbError> {
|
||||
let result = sqlx::query!(
|
||||
"UPDATE users SET takedown_ref = $1 WHERE did = $2",
|
||||
takedown_ref,
|
||||
@@ -1949,7 +1979,10 @@ impl UserRepository for PostgresUserRepository {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_user_for_did_doc_build(&self, did: &Did) -> Result<Option<UserForDidDocBuild>, DbError> {
|
||||
async fn get_user_for_did_doc_build(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserForDidDocBuild>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT id, handle, migrated_to_pds FROM users WHERE did = $1",
|
||||
did.as_str()
|
||||
@@ -2006,7 +2039,10 @@ impl UserRepository for PostgresUserRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_for_passkey_setup(&self, did: &Did) -> Result<Option<UserForPasskeySetup>, DbError> {
|
||||
async fn get_user_for_passkey_setup(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<UserForPasskeySetup>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT id, handle, recovery_token, recovery_token_expires_at, password_required
|
||||
FROM users WHERE did = $1"#,
|
||||
@@ -2111,11 +2147,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn delete_account_with_firehose(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
did: &Did,
|
||||
) -> Result<i64, DbError> {
|
||||
async fn delete_account_with_firehose(&self, user_id: Uuid, did: &Did) -> Result<i64, DbError> {
|
||||
let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?;
|
||||
|
||||
sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
|
||||
@@ -2173,20 +2205,26 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
sqlx::query!("DELETE FROM webauthn_challenges WHERE did = $1", did.as_str())
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM webauthn_challenges WHERE did = $1",
|
||||
did.as_str()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
sqlx::query!("DELETE FROM account_backups WHERE user_id = $1", user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did.as_str())
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM account_deletion_requests WHERE did = $1",
|
||||
did.as_str()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
|
||||
.execute(&mut *tx)
|
||||
@@ -2231,11 +2269,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
tranquil_db_traits::CreatePasswordAccountResult,
|
||||
tranquil_db_traits::CreateAccountError,
|
||||
> {
|
||||
let mut tx = self
|
||||
.pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
let is_first_user: bool = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users")
|
||||
.fetch_one(&mut *tx)
|
||||
@@ -2279,7 +2315,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
return Err(tranquil_db_traits::CreateAccountError::DidExists);
|
||||
}
|
||||
}
|
||||
return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string()));
|
||||
return Err(tranquil_db_traits::CreateAccountError::Database(
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2300,7 +2338,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -2311,7 +2351,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -2325,7 +2367,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
.bind(&input.repo_rev)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
if let Some(code) = &input.invite_code {
|
||||
let _ = sqlx::query!(
|
||||
@@ -2356,9 +2400,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await;
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
tx.commit().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(tranquil_db_traits::CreatePasswordAccountResult {
|
||||
user_id,
|
||||
@@ -2370,11 +2414,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
&self,
|
||||
input: &tranquil_db_traits::CreateDelegatedAccountInput,
|
||||
) -> Result<uuid::Uuid, tranquil_db_traits::CreateAccountError> {
|
||||
let mut tx = self
|
||||
.pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as(
|
||||
r#"INSERT INTO users (
|
||||
@@ -2401,7 +2443,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
return Err(tranquil_db_traits::CreateAccountError::EmailTaken);
|
||||
}
|
||||
}
|
||||
return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string()));
|
||||
return Err(tranquil_db_traits::CreateAccountError::Database(
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2435,7 +2479,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -2449,7 +2495,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
.bind(&input.repo_rev)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
if let Some(code) = &input.invite_code {
|
||||
let _ = sqlx::query!(
|
||||
@@ -2468,9 +2516,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await;
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
tx.commit().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
@@ -2482,11 +2530,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
tranquil_db_traits::CreatePasswordAccountResult,
|
||||
tranquil_db_traits::CreateAccountError,
|
||||
> {
|
||||
let mut tx = self
|
||||
.pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
let is_first_user: bool = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users")
|
||||
.fetch_one(&mut *tx)
|
||||
@@ -2530,7 +2576,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
return Err(tranquil_db_traits::CreateAccountError::EmailTaken);
|
||||
}
|
||||
}
|
||||
return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string()));
|
||||
return Err(tranquil_db_traits::CreateAccountError::Database(
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2551,7 +2599,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -2562,7 +2612,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -2576,7 +2628,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
.bind(&input.repo_rev)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
if let Some(code) = &input.invite_code {
|
||||
let _ = sqlx::query!(
|
||||
@@ -2607,9 +2661,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await;
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
tx.commit().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(tranquil_db_traits::CreatePasswordAccountResult {
|
||||
user_id,
|
||||
@@ -2624,23 +2678,22 @@ impl UserRepository for PostgresUserRepository {
|
||||
tranquil_db_traits::ReactivatedAccountInfo,
|
||||
tranquil_db_traits::MigrationReactivationError,
|
||||
> {
|
||||
let mut tx = self
|
||||
.pool
|
||||
.begin()
|
||||
let mut tx =
|
||||
self.pool.begin().await.map_err(|e| {
|
||||
tranquil_db_traits::MigrationReactivationError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
let existing: Option<(uuid::Uuid, String, Option<chrono::DateTime<chrono::Utc>>)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, handle, deactivated_at FROM users WHERE did = $1 FOR UPDATE",
|
||||
)
|
||||
.bind(input.did.as_str())
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| tranquil_db_traits::MigrationReactivationError::Database(e.to_string()))?;
|
||||
|
||||
let existing: Option<(uuid::Uuid, String, Option<chrono::DateTime<chrono::Utc>>)> =
|
||||
sqlx::query_as("SELECT id, handle, deactivated_at FROM users WHERE did = $1 FOR UPDATE")
|
||||
.bind(input.did.as_str())
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tranquil_db_traits::MigrationReactivationError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
let (account_id, old_handle, deactivated_at) = existing
|
||||
.ok_or(tranquil_db_traits::MigrationReactivationError::NotFound)?;
|
||||
let (account_id, old_handle, deactivated_at) =
|
||||
existing.ok_or(tranquil_db_traits::MigrationReactivationError::NotFound)?;
|
||||
|
||||
if deactivated_at.is_none() {
|
||||
return Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated);
|
||||
@@ -2677,7 +2730,10 @@ impl UserRepository for PostgresUserRepository {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_handle_available_for_new_account(&self, handle: &Handle) -> Result<bool, DbError> {
|
||||
async fn check_handle_available_for_new_account(
|
||||
&self,
|
||||
handle: &Handle,
|
||||
) -> Result<bool, DbError> {
|
||||
let exists: Option<(i32,)> =
|
||||
sqlx::query_as("SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL")
|
||||
.bind(handle.as_str())
|
||||
|
||||
@@ -9,5 +9,3 @@ async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -422,7 +422,10 @@ mod tests {
|
||||
y: Some(y_b64),
|
||||
};
|
||||
let result = verify_es256(&jwk, b"test", &[0u8; 64]);
|
||||
assert!(result.is_err(), "Invalid coordinates should return error, not panic");
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Invalid coordinates should return error, not panic"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,7 +6,6 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-types = { workspace = true }
|
||||
tranquil-infra = { workspace = true }
|
||||
tranquil-crypto = { workspace = true }
|
||||
tranquil-storage = { workspace = true }
|
||||
tranquil-cache = { workspace = true }
|
||||
@@ -21,7 +20,6 @@ tranquil-db-traits = { workspace = true }
|
||||
aes-gcm = { workspace = true }
|
||||
backon = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
aws-config = { workspace = true }
|
||||
aws-sdk-s3 = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
@@ -33,11 +31,9 @@ bytes = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cid = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
governor = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
hickory-resolver = { workspace = true }
|
||||
hkdf = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
@@ -46,17 +42,14 @@ image = { workspace = true }
|
||||
infer = { workspace = true }
|
||||
ipld-core = { workspace = true }
|
||||
iroh-car = { workspace = true }
|
||||
jacquard = { workspace = true }
|
||||
jacquard-axum = { workspace = true }
|
||||
jacquard-common = { workspace = true }
|
||||
jacquard-repo = { workspace = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
k256 = { workspace = true }
|
||||
metrics = { workspace = true }
|
||||
metrics-exporter-prometheus = { workspace = true }
|
||||
multibase = { workspace = true }
|
||||
multihash = { workspace = true }
|
||||
p256 = { workspace = true }
|
||||
p384 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
@@ -72,7 +65,6 @@ subtle = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
totp-rs = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
tower-layer = { workspace = true }
|
||||
@@ -81,7 +73,6 @@ tracing-subscriber = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-rs = { workspace = true }
|
||||
webauthn-rs-proto = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -150,9 +150,9 @@ pub async fn put_preferences(
|
||||
))
|
||||
.into_response(),
|
||||
),
|
||||
PrefValidation::MissingType => Some(
|
||||
ApiError::InvalidRequest("Preference is missing a $type".into()).into_response(),
|
||||
),
|
||||
PrefValidation::MissingType => {
|
||||
Some(ApiError::InvalidRequest("Preference is missing a $type".into()).into_response())
|
||||
}
|
||||
PrefValidation::WrongNamespace => Some(
|
||||
ApiError::InvalidRequest(format!(
|
||||
"Some preferences are not in the {} namespace",
|
||||
@@ -192,10 +192,11 @@ pub async fn put_preferences(
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Err(_) = state
|
||||
if state
|
||||
.infra_repo
|
||||
.replace_namespace_preferences(user_id, APP_BSKY_NAMESPACE, prefs_to_save)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return ApiError::InternalError(Some("Failed to save preferences".into())).into_response();
|
||||
}
|
||||
|
||||
@@ -70,7 +70,11 @@ pub async fn get_account_info(
|
||||
_auth: BearerAuthAdmin,
|
||||
Query(params): Query<GetAccountInfoParams>,
|
||||
) -> Response {
|
||||
let account = match state.infra_repo.get_admin_account_info_by_did(¶ms.did).await {
|
||||
let account = match state
|
||||
.infra_repo
|
||||
.get_admin_account_info_by_did(¶ms.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(a)) => a,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
@@ -114,7 +118,10 @@ async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteC
|
||||
get_invite_code_info(state, &code).await
|
||||
}
|
||||
|
||||
async fn get_invites_for_user(state: &AppState, user_id: uuid::Uuid) -> Option<Vec<InviteCodeInfo>> {
|
||||
async fn get_invites_for_user(
|
||||
state: &AppState,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Option<Vec<InviteCodeInfo>> {
|
||||
let invite_codes = state
|
||||
.infra_repo
|
||||
.get_invites_created_by_user(user_id)
|
||||
@@ -135,10 +142,12 @@ async fn get_invites_for_user(state: &AppState, user_id: uuid::Uuid) -> Option<V
|
||||
|
||||
let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> =
|
||||
uses.into_iter().fold(HashMap::new(), |mut acc, u| {
|
||||
acc.entry(u.code.clone()).or_default().push(InviteCodeUseInfo {
|
||||
used_by: u.used_by_did,
|
||||
used_at: u.used_at.to_rfc3339(),
|
||||
});
|
||||
acc.entry(u.code.clone())
|
||||
.or_default()
|
||||
.push(InviteCodeUseInfo {
|
||||
used_by: u.used_by_did,
|
||||
used_at: u.used_at.to_rfc3339(),
|
||||
});
|
||||
acc
|
||||
});
|
||||
|
||||
@@ -203,11 +212,12 @@ pub async fn get_account_infos(
|
||||
return ApiError::InvalidRequest("dids is required".into()).into_response();
|
||||
}
|
||||
|
||||
let dids_typed: Vec<Did> = dids
|
||||
.iter()
|
||||
.filter_map(|d| d.parse().ok())
|
||||
.collect();
|
||||
let accounts = match state.infra_repo.get_admin_account_infos_by_dids(&dids_typed).await {
|
||||
let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect();
|
||||
let accounts = match state
|
||||
.infra_repo
|
||||
.get_admin_account_infos_by_dids(&dids_typed)
|
||||
.await
|
||||
{
|
||||
Ok(accounts) => accounts,
|
||||
Err(e) => {
|
||||
error!("Failed to fetch account infos: {:?}", e);
|
||||
@@ -223,7 +233,10 @@ pub async fn get_account_infos(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let all_codes: Vec<String> = all_invite_codes.iter().map(|(_, c)| c.code.clone()).collect();
|
||||
let all_codes: Vec<String> = all_invite_codes
|
||||
.iter()
|
||||
.map(|(_, c)| c.code.clone())
|
||||
.collect();
|
||||
|
||||
let all_invite_uses = if !all_codes.is_empty() {
|
||||
state
|
||||
@@ -247,10 +260,12 @@ pub async fn get_account_infos(
|
||||
all_invite_uses
|
||||
.into_iter()
|
||||
.fold(HashMap::new(), |mut acc, u| {
|
||||
acc.entry(u.code.clone()).or_default().push(InviteCodeUseInfo {
|
||||
used_by: u.used_by_did,
|
||||
used_at: u.used_at.to_rfc3339(),
|
||||
});
|
||||
acc.entry(u.code.clone())
|
||||
.or_default()
|
||||
.push(InviteCodeUseInfo {
|
||||
used_by: u.used_by_did,
|
||||
used_at: u.used_at.to_rfc3339(),
|
||||
});
|
||||
acc
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,11 @@ pub async fn update_account_email(
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
match state.user_repo.admin_update_email(&account_did, email).await {
|
||||
match state
|
||||
.user_repo
|
||||
.admin_update_email(&account_did, email)
|
||||
.await
|
||||
{
|
||||
Ok(0) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(_) => EmptyResponse::ok().into_response(),
|
||||
Err(e) => {
|
||||
@@ -70,30 +74,36 @@ pub async fn update_account_handle(
|
||||
} else {
|
||||
input_handle.to_string()
|
||||
};
|
||||
let old_handle = state
|
||||
.user_repo
|
||||
.get_handle_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten();
|
||||
let user_id = match state.user_repo.get_id_by_did(did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => return ApiError::AccountNotFound.into_response(),
|
||||
};
|
||||
let handle_for_check = Handle::new_unchecked(&handle);
|
||||
if let Ok(true) = state.user_repo.check_handle_exists(&handle_for_check, user_id).await {
|
||||
if let Ok(true) = state
|
||||
.user_repo
|
||||
.check_handle_exists(&handle_for_check, user_id)
|
||||
.await
|
||||
{
|
||||
return ApiError::HandleTaken.into_response();
|
||||
}
|
||||
match state.user_repo.admin_update_handle(did, &handle_for_check).await {
|
||||
match state
|
||||
.user_repo
|
||||
.admin_update_handle(did, &handle_for_check)
|
||||
.await
|
||||
{
|
||||
Ok(0) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(_) => {
|
||||
if let Some(old) = old_handle {
|
||||
let _ = state.cache.delete(&format!("handle:{}", old)).await;
|
||||
}
|
||||
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, did, Some(&handle_for_check))
|
||||
.await
|
||||
if let Err(e) = crate::api::repo::record::sequence_identity_event(
|
||||
&state,
|
||||
did,
|
||||
Some(&handle_for_check),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to sequence identity event for admin handle update: {}",
|
||||
@@ -137,7 +147,11 @@ pub async fn update_account_password(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
match state.user_repo.admin_update_password(did, &password_hash).await {
|
||||
match state
|
||||
.user_repo
|
||||
.admin_update_password(did, &password_hash)
|
||||
.await
|
||||
{
|
||||
Ok(0) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(_) => EmptyResponse::ok().into_response(),
|
||||
Err(e) => {
|
||||
|
||||
@@ -24,16 +24,14 @@ pub async fn disable_invite_codes(
|
||||
_auth: BearerAuthAdmin,
|
||||
Json(input): Json<DisableInviteCodesInput>,
|
||||
) -> Response {
|
||||
if let Some(codes) = &input.codes {
|
||||
if let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await {
|
||||
error!("DB error disabling invite codes: {:?}", e);
|
||||
}
|
||||
if let Some(codes) = &input.codes
|
||||
&& let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await
|
||||
{
|
||||
error!("DB error disabling invite codes: {:?}", e);
|
||||
}
|
||||
if let Some(accounts) = &input.accounts {
|
||||
let accounts_typed: Vec<tranquil_types::Did> = accounts
|
||||
.iter()
|
||||
.filter_map(|a| a.parse().ok())
|
||||
.collect();
|
||||
let accounts_typed: Vec<tranquil_types::Did> =
|
||||
accounts.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
if let Err(e) = state
|
||||
.infra_repo
|
||||
.disable_invite_codes_by_account(&accounts_typed)
|
||||
@@ -112,25 +110,26 @@ pub async fn get_invite_codes(
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> = if code_strings
|
||||
.is_empty()
|
||||
{
|
||||
std::collections::HashMap::new()
|
||||
} else {
|
||||
state
|
||||
.infra_repo
|
||||
.get_invite_code_uses_batch(&code_strings)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.fold(std::collections::HashMap::new(), |mut acc, u| {
|
||||
acc.entry(u.code.clone()).or_default().push(InviteCodeUseInfo {
|
||||
used_by: u.used_by_did.to_string(),
|
||||
used_at: u.used_at.to_rfc3339(),
|
||||
});
|
||||
acc
|
||||
})
|
||||
};
|
||||
let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> =
|
||||
if code_strings.is_empty() {
|
||||
std::collections::HashMap::new()
|
||||
} else {
|
||||
state
|
||||
.infra_repo
|
||||
.get_invite_code_uses_batch(&code_strings)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.fold(std::collections::HashMap::new(), |mut acc, u| {
|
||||
acc.entry(u.code.clone())
|
||||
.or_default()
|
||||
.push(InviteCodeUseInfo {
|
||||
used_by: u.used_by_did.to_string(),
|
||||
used_at: u.used_at.to_rfc3339(),
|
||||
});
|
||||
acc
|
||||
})
|
||||
};
|
||||
|
||||
let codes: Vec<InviteCodeInfo> = codes_rows
|
||||
.iter()
|
||||
@@ -184,7 +183,11 @@ pub async fn disable_account_invites(
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
match state.user_repo.set_invites_disabled(&account_did, true).await {
|
||||
match state
|
||||
.user_repo
|
||||
.set_invites_disabled(&account_did, true)
|
||||
.await
|
||||
{
|
||||
Ok(true) => EmptyResponse::ok().into_response(),
|
||||
Ok(false) => ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
@@ -212,7 +215,11 @@ pub async fn enable_account_invites(
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
match state.user_repo.set_invites_disabled(&account_did, false).await {
|
||||
match state
|
||||
.user_repo
|
||||
.set_invites_disabled(&account_did, false)
|
||||
.await
|
||||
{
|
||||
Ok(true) => EmptyResponse::ok().into_response(),
|
||||
Ok(false) => ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
|
||||
@@ -187,11 +187,7 @@ pub async fn update_subject_status(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.set_user_takedown(&did, takedown_ref)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = state.user_repo.set_user_takedown(&did, takedown_ref).await {
|
||||
error!("Failed to update user takedown status for {}: {:?}", did, e);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to update takedown status".into(),
|
||||
@@ -274,7 +270,10 @@ pub async fn update_subject_status(
|
||||
if let Some(uri_str) = uri_str {
|
||||
let cid: CidLink = match uri_str.parse() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid CID format".into())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let takedown_ref = if takedown.applied {
|
||||
@@ -315,7 +314,10 @@ pub async fn update_subject_status(
|
||||
if let Some(cid_str) = cid_str {
|
||||
let cid: CidLink = match cid_str.parse() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid CID format".into())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let takedown_ref = if takedown.applied {
|
||||
@@ -328,7 +330,10 @@ pub async fn update_subject_status(
|
||||
.update_blob_takedown(&cid, takedown_ref)
|
||||
.await
|
||||
{
|
||||
error!("Failed to update blob takedown status for {}: {:?}", cid_str, e);
|
||||
error!(
|
||||
"Failed to update blob takedown status for {}: {:?}",
|
||||
cid_str, e
|
||||
);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to update takedown status".into(),
|
||||
))
|
||||
|
||||
@@ -14,8 +14,8 @@ use cid::Cid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use tranquil_db::{BackupRepository, OldBackupInfo};
|
||||
use tracing::{error, info, warn};
|
||||
use tranquil_db::{BackupRepository, OldBackupInfo};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -36,16 +36,17 @@ pub struct ListBackupsOutput {
|
||||
}
|
||||
|
||||
pub async fn list_backups(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.0.did).await {
|
||||
Ok(Some(status)) => status,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let (user_id, backup_enabled) =
|
||||
match state.backup_repo.get_user_backup_status(&auth.0.did).await {
|
||||
Ok(Some(status)) => status,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let backups = match state.backup_repo.list_backups_for_user(user_id).await {
|
||||
Ok(rows) => rows,
|
||||
@@ -94,7 +95,11 @@ pub async fn get_backup(
|
||||
}
|
||||
};
|
||||
|
||||
let backup_info = match state.backup_repo.get_backup_storage_info(backup_id, &auth.0.did).await {
|
||||
let backup_info = match state
|
||||
.backup_repo
|
||||
.get_backup_storage_info(backup_id, &auth.0.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
return ApiError::BackupNotFound.into_response();
|
||||
@@ -181,15 +186,21 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
}
|
||||
};
|
||||
|
||||
let car_bytes =
|
||||
match generate_full_backup(state.repo_repo.as_ref(), &state.block_store, user.id, &head_cid).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Failed to generate CAR: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to generate backup".into()))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let car_bytes = match generate_full_backup(
|
||||
state.repo_repo.as_ref(),
|
||||
&state.block_store,
|
||||
user.id,
|
||||
&head_cid,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Failed to generate CAR: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to generate backup".into()))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let block_count = crate::scheduled::count_car_blocks(&car_bytes);
|
||||
let size_bytes = car_bytes.len() as i64;
|
||||
@@ -205,14 +216,18 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
}
|
||||
};
|
||||
|
||||
let backup_id = match state.backup_repo.insert_backup(
|
||||
user.id,
|
||||
&storage_key,
|
||||
&user.repo_root_cid,
|
||||
&repo_rev,
|
||||
block_count,
|
||||
size_bytes,
|
||||
).await {
|
||||
let backup_id = match state
|
||||
.backup_repo
|
||||
.insert_backup(
|
||||
user.id,
|
||||
&storage_key,
|
||||
&user.repo_root_cid,
|
||||
&repo_rev,
|
||||
block_count,
|
||||
size_bytes,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
error!("DB error inserting backup: {:?}", e);
|
||||
@@ -235,7 +250,14 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
);
|
||||
|
||||
let retention = BackupStorage::retention_count();
|
||||
if let Err(e) = cleanup_old_backups(state.backup_repo.as_ref(), backup_storage, user.id, retention).await {
|
||||
if let Err(e) = cleanup_old_backups(
|
||||
state.backup_repo.as_ref(),
|
||||
backup_storage,
|
||||
user.id,
|
||||
retention,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup");
|
||||
}
|
||||
|
||||
@@ -298,7 +320,11 @@ pub async fn delete_backup(
|
||||
}
|
||||
};
|
||||
|
||||
let backup = match state.backup_repo.get_backup_for_deletion(backup_id, &auth.0.did).await {
|
||||
let backup = match state
|
||||
.backup_repo
|
||||
.get_backup_for_deletion(backup_id, &auth.0.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
return ApiError::BackupNotFound.into_response();
|
||||
@@ -344,7 +370,11 @@ pub async fn set_backup_enabled(
|
||||
auth: BearerAuth,
|
||||
Json(input): Json<SetBackupEnabledInput>,
|
||||
) -> Response {
|
||||
let deactivated_at = match state.backup_repo.get_user_deactivated_status(&auth.0.did).await {
|
||||
let deactivated_at = match state
|
||||
.backup_repo
|
||||
.get_user_deactivated_status(&auth.0.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(status)) => status,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
@@ -359,7 +389,11 @@ pub async fn set_backup_enabled(
|
||||
return ApiError::AccountDeactivated.into_response();
|
||||
}
|
||||
|
||||
if let Err(e) = state.backup_repo.update_backup_enabled(&auth.0.did, input.enabled).await {
|
||||
if let Err(e) = state
|
||||
.backup_repo
|
||||
.update_backup_enabled(&auth.0.did, input.enabled)
|
||||
.await
|
||||
{
|
||||
error!("DB error updating backup_enabled: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to update setting".into())).into_response();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -51,8 +51,8 @@ pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -
|
||||
controllers: controllers
|
||||
.into_iter()
|
||||
.map(|c| ControllerInfo {
|
||||
did: c.did.into(),
|
||||
handle: c.handle.into(),
|
||||
did: c.did,
|
||||
handle: c.handle,
|
||||
granted_scopes: c.granted_scopes,
|
||||
granted_at: c.granted_at,
|
||||
is_active: c.is_active,
|
||||
@@ -89,7 +89,11 @@ pub async fn add_controller(
|
||||
return ApiError::ControllerNotFound.into_response();
|
||||
}
|
||||
|
||||
match state.delegation_repo.controls_any_accounts(&auth.0.did).await {
|
||||
match state
|
||||
.delegation_repo
|
||||
.controls_any_accounts(&auth.0.did)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
return ApiError::InvalidDelegation(
|
||||
"Cannot add controllers to an account that controls other accounts".into(),
|
||||
@@ -309,8 +313,8 @@ pub async fn list_controlled_accounts(State(state): State<AppState>, auth: Beare
|
||||
accounts: accounts
|
||||
.into_iter()
|
||||
.map(|a| DelegatedAccountInfo {
|
||||
did: a.did.into(),
|
||||
handle: a.handle.into(),
|
||||
did: a.did,
|
||||
handle: a.handle,
|
||||
granted_scopes: a.granted_scopes,
|
||||
granted_at: a.granted_at,
|
||||
})
|
||||
@@ -380,9 +384,9 @@ pub async fn get_audit_log(
|
||||
.into_iter()
|
||||
.map(|e| AuditLogEntry {
|
||||
id: e.id.to_string(),
|
||||
delegated_did: e.delegated_did.into(),
|
||||
actor_did: e.actor_did.into(),
|
||||
controller_did: e.controller_did.map(Into::into),
|
||||
delegated_did: e.delegated_did,
|
||||
actor_did: e.actor_did,
|
||||
controller_did: e.controller_did,
|
||||
action_type: format!("{:?}", e.action_type),
|
||||
action_details: e.action_details,
|
||||
created_at: e.created_at,
|
||||
@@ -509,7 +513,11 @@ pub async fn create_delegated_account(
|
||||
}
|
||||
|
||||
if let Some(ref code) = input.invite_code {
|
||||
let valid = state.infra_repo.is_invite_code_valid(code).await.unwrap_or(false);
|
||||
let valid = state
|
||||
.infra_repo
|
||||
.is_invite_code_valid(code)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if !valid {
|
||||
return ApiError::InvalidInviteCode.into_response();
|
||||
@@ -620,7 +628,11 @@ pub async fn create_delegated_account(
|
||||
invite_code: input.invite_code.clone(),
|
||||
};
|
||||
|
||||
let _user_id = match state.user_repo.create_delegated_account(&create_input).await {
|
||||
let _user_id = match state
|
||||
.user_repo
|
||||
.create_delegated_account(&create_input)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return ApiError::HandleNotAvailable(None).into_response();
|
||||
|
||||
@@ -398,6 +398,11 @@ impl ApiError {
|
||||
Self::UpstreamError { message, .. } => message.clone(),
|
||||
Self::UpstreamTimeout => Some("Upstream service timed out".to_string()),
|
||||
Self::AdminRequired => Some("This action requires admin privileges".to_string()),
|
||||
Self::EmailTaken => Some("This email address is already registered".to_string()),
|
||||
Self::HandleTaken => Some("This handle is already taken".to_string()),
|
||||
Self::InvalidEmail => Some("Please provide a valid email address".to_string()),
|
||||
Self::InvalidInviteCode => Some("The invite code provided is invalid".to_string()),
|
||||
Self::DuplicateCreate => Some("Account creation failed: duplicate request".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -482,7 +487,6 @@ impl From<crate::auth::TokenValidationError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<crate::auth::extractor::AuthError> for ApiError {
|
||||
fn from(e: crate::auth::extractor::AuthError) -> Self {
|
||||
match e {
|
||||
@@ -566,8 +570,8 @@ impl From<crate::api::validation::HandleValidationError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jacquard::types::string::AtStrError> for ApiError {
|
||||
fn from(e: jacquard::types::string::AtStrError) -> Self {
|
||||
impl From<jacquard_common::types::string::AtStrError> for ApiError {
|
||||
fn from(e: jacquard_common::types::string::AtStrError) -> Self {
|
||||
Self::InvalidRequest(format!("Invalid {}: {}", e.spec, e.kind))
|
||||
}
|
||||
}
|
||||
@@ -637,11 +641,13 @@ impl From<crate::storage::StorageError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn parse_did(s: &str) -> Result<tranquil_types::Did, Response> {
|
||||
s.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()).into_response())
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn parse_did_option(s: Option<&str>) -> Result<Option<tranquil_types::Did>, Response> {
|
||||
s.map(parse_did).transpose()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use k256::{SecretKey, ecdsa::SigningKey};
|
||||
use rand::rngs::OsRng;
|
||||
@@ -255,7 +255,11 @@ pub async fn create_account(
|
||||
};
|
||||
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
|
||||
if let Some(signing_key_did) = &input.signing_key {
|
||||
match state.infra_repo.get_reserved_signing_key(signing_key_did).await {
|
||||
match state
|
||||
.infra_repo
|
||||
.get_reserved_signing_key(signing_key_did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(key)) => (key.private_key_bytes, Some(key.id)),
|
||||
Ok(None) => {
|
||||
return ApiError::InvalidSigningKey.into_response();
|
||||
@@ -407,12 +411,23 @@ pub async fn create_account(
|
||||
did: Did::new_unchecked(&did),
|
||||
new_handle: Handle::new_unchecked(&handle),
|
||||
};
|
||||
match state.user_repo.reactivate_migration_account(&reactivate_input).await {
|
||||
match state
|
||||
.user_repo
|
||||
.reactivate_migration_account(&reactivate_input)
|
||||
.await
|
||||
{
|
||||
Ok(reactivated) => {
|
||||
info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration");
|
||||
let secret_key_bytes = match state.user_repo.get_user_key_by_id(reactivated.user_id).await {
|
||||
let secret_key_bytes = match state
|
||||
.user_repo
|
||||
.get_user_key_by_id(reactivated.user_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(key_info)) => {
|
||||
match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
match crate::config::decrypt_key(
|
||||
&key_info.key_bytes,
|
||||
key_info.encryption_version,
|
||||
) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Error decrypting key for reactivated account: {:?}", e);
|
||||
@@ -476,8 +491,7 @@ pub async fn create_account(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(tranquil_db_traits::MigrationReactivationError::NotFound) => {
|
||||
}
|
||||
Err(tranquil_db_traits::MigrationReactivationError::NotFound) => {}
|
||||
Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated) => {
|
||||
return ApiError::AccountAlreadyExists.into_response();
|
||||
}
|
||||
@@ -492,7 +506,11 @@ pub async fn create_account(
|
||||
}
|
||||
|
||||
let handle_typed = Handle::new_unchecked(&handle);
|
||||
let handle_available = match state.user_repo.check_handle_available_for_new_account(&handle_typed).await {
|
||||
let handle_available = match state
|
||||
.user_repo
|
||||
.check_handle_available_for_new_account(&handle_typed)
|
||||
.await
|
||||
{
|
||||
Ok(available) => available,
|
||||
Err(e) => {
|
||||
error!("Error checking handle availability: {:?}", e);
|
||||
@@ -612,9 +630,24 @@ pub async fn create_account(
|
||||
did: Did::new_unchecked(&did),
|
||||
password_hash,
|
||||
preferred_comms_channel,
|
||||
discord_id: input.discord_id.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from),
|
||||
telegram_username: input.telegram_username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from),
|
||||
signal_number: input.signal_number.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from),
|
||||
discord_id: input
|
||||
.discord_id
|
||||
.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
telegram_username: input
|
||||
.telegram_username
|
||||
.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
signal_number: input
|
||||
.signal_number
|
||||
.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
deactivated_at,
|
||||
encrypted_key_bytes,
|
||||
encryption_version: crate::config::ENCRYPTION_VERSION,
|
||||
|
||||
@@ -44,7 +44,9 @@ pub async fn resolve_handle(
|
||||
}
|
||||
let handle: Handle = match handle_str.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
|
||||
}
|
||||
};
|
||||
let user = state.user_repo.get_by_handle(&handle).await;
|
||||
match user {
|
||||
@@ -131,14 +133,20 @@ pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response {
|
||||
async fn serve_subdomain_did_doc(state: &AppState, subdomain: &str, hostname: &str) -> Response {
|
||||
let hostname_for_handles = hostname.split(':').next().unwrap_or(hostname);
|
||||
let full_handle = format!("{}.{}", handle, hostname_for_handles);
|
||||
let full_handle_typed: Handle = match full_handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(),
|
||||
let subdomain_host = format!("{}.{}", subdomain, hostname_for_handles);
|
||||
let encoded_subdomain = subdomain_host.replace(':', "%3A");
|
||||
let expected_did = format!("did:web:{}", encoded_subdomain);
|
||||
let expected_did_typed: crate::types::Did = match expected_did.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
let user = match state.user_repo.get_did_web_info_by_handle(&full_handle_typed).await {
|
||||
let user = match state
|
||||
.user_repo
|
||||
.get_user_for_did_doc_build(&expected_did_typed)
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::NotFoundMsg("User not found".into()).into_response();
|
||||
@@ -148,17 +156,8 @@ async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str)
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let (user_id, did, migrated_to_pds) = (user.id, user.did, user.migrated_to_pds);
|
||||
if !did.starts_with("did:web:") {
|
||||
return ApiError::NotFoundMsg("User is not did:web".into()).into_response();
|
||||
}
|
||||
let subdomain_host = format!("{}.{}", handle, hostname_for_handles);
|
||||
let encoded_subdomain = subdomain_host.replace(':', "%3A");
|
||||
let expected_self_hosted = format!("did:web:{}", encoded_subdomain);
|
||||
if did != expected_self_hosted {
|
||||
return ApiError::NotFoundMsg("External did:web - DID document hosted by user".into())
|
||||
.into_response();
|
||||
}
|
||||
let (user_id, current_handle, migrated_to_pds) = (user.id, user.handle, user.migrated_to_pds);
|
||||
let did = expected_did;
|
||||
|
||||
let overrides = state
|
||||
.user_repo
|
||||
@@ -178,7 +177,7 @@ async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str)
|
||||
let also_known_as = if !ovr.also_known_as.is_empty() {
|
||||
ovr.also_known_as.clone()
|
||||
} else {
|
||||
vec![format!("at://{}", full_handle)]
|
||||
vec![format!("at://{}", current_handle)]
|
||||
};
|
||||
|
||||
return Json(json!({
|
||||
@@ -209,12 +208,13 @@ async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str)
|
||||
Ok(None) => return ApiError::InternalError(None).into_response(),
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
let key_bytes: Vec<u8> = match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes: Vec<u8> =
|
||||
match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let public_key_multibase = match get_public_key_multibase(&key_bytes) {
|
||||
Ok(pk) => pk,
|
||||
Err(e) => {
|
||||
@@ -227,10 +227,10 @@ async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str)
|
||||
if !ovr.also_known_as.is_empty() {
|
||||
ovr.also_known_as.clone()
|
||||
} else {
|
||||
vec![format!("at://{}", full_handle)]
|
||||
vec![format!("at://{}", current_handle)]
|
||||
}
|
||||
} else {
|
||||
vec![format!("at://{}", full_handle)]
|
||||
vec![format!("at://{}", current_handle)]
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
@@ -259,12 +259,18 @@ async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str)
|
||||
pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response {
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname);
|
||||
let full_handle = format!("{}.{}", handle, hostname_for_handles);
|
||||
let full_handle_typed: Handle = match full_handle.parse() {
|
||||
let current_handle = format!("{}.{}", handle, hostname_for_handles);
|
||||
let current_handle_typed: Handle = match current_handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
|
||||
}
|
||||
};
|
||||
let user = match state.user_repo.get_did_web_info_by_handle(&full_handle_typed).await {
|
||||
let user = match state
|
||||
.user_repo
|
||||
.get_did_web_info_by_handle(¤t_handle_typed)
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::NotFoundMsg("User not found".into()).into_response();
|
||||
@@ -306,7 +312,7 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
let also_known_as = if !ovr.also_known_as.is_empty() {
|
||||
ovr.also_known_as.clone()
|
||||
} else {
|
||||
vec![format!("at://{}", full_handle)]
|
||||
vec![format!("at://{}", current_handle)]
|
||||
};
|
||||
|
||||
return Json(json!({
|
||||
@@ -337,12 +343,13 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
Ok(None) => return ApiError::InternalError(None).into_response(),
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
let key_bytes: Vec<u8> = match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes: Vec<u8> =
|
||||
match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let public_key_multibase = match get_public_key_multibase(&key_bytes) {
|
||||
Ok(pk) => pk,
|
||||
Err(e) => {
|
||||
@@ -355,10 +362,10 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
if !ovr.also_known_as.is_empty() {
|
||||
ovr.also_known_as.clone()
|
||||
} else {
|
||||
vec![format!("at://{}", full_handle)]
|
||||
vec![format!("at://{}", current_handle)]
|
||||
}
|
||||
} else {
|
||||
vec![format!("at://{}", full_handle)]
|
||||
vec![format!("at://{}", current_handle)]
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
@@ -639,8 +646,9 @@ pub async fn update_handle(
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname);
|
||||
let suffix = format!(".{}", hostname_for_handles);
|
||||
let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles);
|
||||
let handle = if is_service_domain {
|
||||
let is_service_domain =
|
||||
crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles);
|
||||
let handle = if is_service_domain && new_handle != hostname_for_handles {
|
||||
let short_part = if new_handle.ends_with(&suffix) {
|
||||
new_handle.strip_suffix(&suffix).unwrap_or(&new_handle)
|
||||
} else {
|
||||
@@ -710,9 +718,15 @@ pub async fn update_handle(
|
||||
};
|
||||
let handle_typed: Handle = match handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
|
||||
}
|
||||
};
|
||||
let handle_exists = match state.user_repo.check_handle_exists(&handle_typed, user_id).await {
|
||||
let handle_exists = match state
|
||||
.user_repo
|
||||
.check_handle_exists(&handle_typed, user_id)
|
||||
.await
|
||||
{
|
||||
Ok(exists) => exists,
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
|
||||
@@ -71,33 +71,32 @@ async fn proxy_to_report_service(
|
||||
|
||||
let key_bytes = match &auth_user.key_bytes {
|
||||
Some(kb) => kb.clone(),
|
||||
None => {
|
||||
match state.user_repo.get_with_key_by_did(&auth_user.did).await {
|
||||
Ok(Some(user_with_key)) => {
|
||||
match crate::config::decrypt_key(&user_with_key.key_bytes, user_with_key.encryption_version) {
|
||||
Ok(key) => key,
|
||||
Err(e) => {
|
||||
error!(error = ?e, "Failed to decrypt user key for report service auth");
|
||||
return ApiError::AuthenticationFailed(Some(
|
||||
"Failed to get signing key".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
None => match state.user_repo.get_with_key_by_did(&auth_user.did).await {
|
||||
Ok(Some(user_with_key)) => {
|
||||
match crate::config::decrypt_key(
|
||||
&user_with_key.key_bytes,
|
||||
user_with_key.encryption_version,
|
||||
) {
|
||||
Ok(key) => key,
|
||||
Err(e) => {
|
||||
error!(error = ?e, "Failed to decrypt user key for report service auth");
|
||||
return ApiError::AuthenticationFailed(Some(
|
||||
"Failed to get signing key".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
return ApiError::AuthenticationFailed(Some("User has no signing key".into()))
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = ?e, "DB error fetching user key for report");
|
||||
return ApiError::AuthenticationFailed(Some(
|
||||
"Failed to get signing key".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
return ApiError::AuthenticationFailed(Some("User has no signing key".into()))
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = ?e, "DB error fetching user key for report");
|
||||
return ApiError::AuthenticationFailed(Some("Failed to get signing key".into()))
|
||||
.into_response();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let service_token = match crate::auth::create_service_token(
|
||||
@@ -205,14 +204,18 @@ async fn create_report_locally(
|
||||
let report_id = (uuid::Uuid::now_v7().as_u128() & 0x7FFF_FFFF_FFFF_FFFF) as i64;
|
||||
let subject_json = json!(input.subject);
|
||||
|
||||
if let Err(e) = state.infra_repo.insert_report(
|
||||
report_id,
|
||||
&input.reason_type,
|
||||
input.reason.as_deref(),
|
||||
subject_json,
|
||||
did,
|
||||
created_at,
|
||||
).await {
|
||||
if let Err(e) = state
|
||||
.infra_repo
|
||||
.insert_report(
|
||||
report_id,
|
||||
&input.reason_type,
|
||||
input.reason.as_deref(),
|
||||
subject_json,
|
||||
did,
|
||||
created_at,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to insert report: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
|
||||
@@ -189,11 +189,7 @@ pub async fn update_notification_prefs(
|
||||
) -> Response {
|
||||
let user = auth.0;
|
||||
|
||||
let user_row = match state
|
||||
.user_repo
|
||||
.get_id_handle_email_by_did(&user.did)
|
||||
.await
|
||||
{
|
||||
let user_row = match state.user_repo.get_id_handle_email_by_did(&user.did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
@@ -238,7 +234,11 @@ pub async fn update_notification_prefs(
|
||||
if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email_clean.clone()) {
|
||||
info!(did = %user.did, "Email unchanged, skipping");
|
||||
} else {
|
||||
match state.user_repo.check_email_exists(&email_clean, user_id).await {
|
||||
match state
|
||||
.user_repo
|
||||
.check_email_exists(&email_clean, user_id)
|
||||
.await
|
||||
{
|
||||
Ok(true) => return ApiError::EmailTaken.into_response(),
|
||||
Err(e) => {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e)))
|
||||
@@ -272,9 +272,10 @@ pub async fn update_notification_prefs(
|
||||
}
|
||||
info!(did = %user.did, "Cleared Discord ID");
|
||||
} else {
|
||||
if let Err(e) =
|
||||
request_channel_verification(&state, user_id, &user.did, "discord", discord_id, None)
|
||||
.await
|
||||
if let Err(e) = request_channel_verification(
|
||||
&state, user_id, &user.did, "discord", discord_id, None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ApiError::InternalError(Some(e)).into_response();
|
||||
}
|
||||
|
||||
@@ -150,10 +150,9 @@ impl<S: Service<Request, Response = Response, Error = Infallible>> Service<Reque
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request) -> Self::Future {
|
||||
if req
|
||||
.headers()
|
||||
.contains_key(http::HeaderName::from(jacquard::xrpc::Header::AtprotoProxy))
|
||||
{
|
||||
if req.headers().contains_key(http::HeaderName::from(
|
||||
jacquard_common::xrpc::Header::AtprotoProxy,
|
||||
)) {
|
||||
let path = req.uri().path();
|
||||
let method = path.trim_start_matches("/");
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ pub async fn upload_blob(
|
||||
debug!("Service token verified for DID: {}", claims.iss);
|
||||
let did: Did = match claims.iss.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidDid("Invalid DID format".into()).into_response();
|
||||
}
|
||||
};
|
||||
(did, false, None)
|
||||
}
|
||||
@@ -217,12 +219,10 @@ pub async fn upload_blob(
|
||||
}
|
||||
};
|
||||
|
||||
if was_inserted {
|
||||
if let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await {
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
error!("Failed to copy blob to final location: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to store blob".into())).into_response();
|
||||
}
|
||||
if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await {
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
error!("Failed to copy blob to final location: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to store blob".into())).into_response();
|
||||
}
|
||||
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
|
||||
@@ -6,17 +6,17 @@ use crate::state::AppState;
|
||||
use crate::sync::import::{ImportError, apply_import, parse_car};
|
||||
use crate::sync::verify::CarVerifier;
|
||||
use crate::types::Did;
|
||||
use tranquil_types::{AtUri, CidLink};
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::storage::BlockStore;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use serde_json::json;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tranquil_types::{AtUri, CidLink};
|
||||
|
||||
const DEFAULT_MAX_IMPORT_SIZE: usize = 1024 * 1024 * 1024;
|
||||
const DEFAULT_MAX_BLOCKS: usize = 500000;
|
||||
@@ -196,11 +196,14 @@ pub async fn import_repo(
|
||||
.records
|
||||
.iter()
|
||||
.flat_map(|record| {
|
||||
let record_uri = AtUri::from_parts(did.as_str(), &record.collection, &record.rkey);
|
||||
record
|
||||
.blob_refs
|
||||
.iter()
|
||||
.map(move |blob_ref| (record_uri.clone(), CidLink::new_unchecked(blob_ref.cid.clone())))
|
||||
let record_uri =
|
||||
AtUri::from_parts(did.as_str(), &record.collection, &record.rkey);
|
||||
record.blob_refs.iter().map(move |blob_ref| {
|
||||
(
|
||||
record_uri.clone(),
|
||||
CidLink::new_unchecked(blob_ref.cid.clone()),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -273,7 +276,7 @@ pub async fn import_repo(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let new_root_cid_link = CidLink::new_unchecked(&new_root_cid.to_string());
|
||||
let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string());
|
||||
if let Err(e) = state
|
||||
.repo_repo
|
||||
.update_repo_root(user_id, &new_root_cid_link, &new_rev_str)
|
||||
|
||||
@@ -39,7 +39,9 @@ pub async fn describe_repo(
|
||||
};
|
||||
let handle: crate::types::Handle = match handle_str.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid handle format".into()).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid handle format".into()).into_response();
|
||||
}
|
||||
};
|
||||
state
|
||||
.user_repo
|
||||
|
||||
@@ -378,19 +378,11 @@ pub async fn apply_writes(
|
||||
}
|
||||
}
|
||||
|
||||
let user_id: uuid::Uuid = match state
|
||||
.user_repo
|
||||
.get_id_by_did(&did)
|
||||
.await
|
||||
{
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => return ApiError::InternalError(Some("User not found".into())).into_response(),
|
||||
};
|
||||
let root_cid_str = match state
|
||||
.repo_repo
|
||||
.get_repo_root_cid_by_user_id(user_id)
|
||||
.await
|
||||
{
|
||||
let root_cid_str = match state.repo_repo.get_repo_root_cid_by_user_id(user_id).await {
|
||||
Ok(Some(cid_str)) => cid_str,
|
||||
_ => return ApiError::InternalError(Some("Repo root not found".into())).into_response(),
|
||||
};
|
||||
|
||||
@@ -202,7 +202,11 @@ pub async fn delete_record(
|
||||
}
|
||||
|
||||
let deleted_uri = AtUri::from_parts(&did, &input.collection, &input.rkey);
|
||||
if let Err(e) = state.backlink_repo.remove_backlinks_by_uri(&deleted_uri).await {
|
||||
if let Err(e) = state
|
||||
.backlink_repo
|
||||
.remove_backlinks_by_uri(&deleted_uri)
|
||||
.await
|
||||
{
|
||||
error!("Failed to remove backlinks for {}: {}", deleted_uri, e);
|
||||
}
|
||||
|
||||
@@ -245,8 +249,8 @@ pub async fn delete_record_internal(
|
||||
.map_err(|e| format!("Failed to fetch commit: {:?}", e))?
|
||||
.ok_or_else(|| "Commit block not found".to_string())?;
|
||||
|
||||
let commit = Commit::from_cbor(&commit_bytes)
|
||||
.map_err(|e| format!("Failed to parse commit: {:?}", e))?;
|
||||
let commit =
|
||||
Commit::from_cbor(&commit_bytes).map_err(|e| format!("Failed to parse commit: {:?}", e))?;
|
||||
|
||||
let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let key = format!("{}/{}", collection, rkey);
|
||||
|
||||
@@ -65,11 +65,7 @@ pub async fn get_record(
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
state
|
||||
.user_repo
|
||||
.get_id_by_did(&did)
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
state.user_repo.get_id_by_did(&did).await.map_err(|_| ())
|
||||
} else {
|
||||
let repo_str = input.repo.as_str();
|
||||
let handle_str = if !repo_str.contains('.') {
|
||||
@@ -79,7 +75,9 @@ pub async fn get_record(
|
||||
};
|
||||
let handle: crate::types::Handle = match handle_str.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid handle format".into()).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid handle format".into()).into_response();
|
||||
}
|
||||
};
|
||||
state
|
||||
.user_repo
|
||||
@@ -166,11 +164,7 @@ pub async fn list_records(
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
state
|
||||
.user_repo
|
||||
.get_id_by_did(&did)
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
state.user_repo.get_id_by_did(&did).await.map_err(|_| ())
|
||||
} else {
|
||||
let repo_str = input.repo.as_str();
|
||||
let handle_str = if !repo_str.contains('.') {
|
||||
@@ -180,7 +174,9 @@ pub async fn list_records(
|
||||
};
|
||||
let handle: crate::types::Handle = match handle_str.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid handle format".into()).into_response(),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid handle format".into()).into_response();
|
||||
}
|
||||
};
|
||||
state
|
||||
.user_repo
|
||||
@@ -200,7 +196,10 @@ pub async fn list_records(
|
||||
let limit = input.limit.unwrap_or(50).clamp(1, 100);
|
||||
let reverse = input.reverse.unwrap_or(false);
|
||||
let limit_i64 = limit as i64;
|
||||
let cursor_rkey = input.cursor.as_ref().and_then(|c| c.parse::<crate::types::Rkey>().ok());
|
||||
let cursor_rkey = input
|
||||
.cursor
|
||||
.as_ref()
|
||||
.and_then(|c| c.parse::<crate::types::Rkey>().ok());
|
||||
let rows = match state
|
||||
.repo_repo
|
||||
.list_records(
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, Nsid, Rkey};
|
||||
use bytes::Bytes;
|
||||
use cid::Cid;
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::commit::Commit;
|
||||
use jacquard_repo::storage::BlockStore;
|
||||
use k256::ecdsa::SigningKey;
|
||||
@@ -36,8 +36,8 @@ fn extract_blob_cids_recursive(value: &Value, blobs: &mut Vec<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
use tranquil_db_traits::Backlink;
|
||||
use crate::types::AtUri;
|
||||
use tranquil_db_traits::Backlink;
|
||||
|
||||
pub fn extract_backlinks(uri: &AtUri, record: &Value) -> Vec<Backlink> {
|
||||
let record_type = record
|
||||
@@ -50,22 +50,26 @@ pub fn extract_backlinks(uri: &AtUri, record: &Value) -> Vec<Backlink> {
|
||||
.get("subject")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| s.starts_with("did:"))
|
||||
.map(|subject| vec![Backlink {
|
||||
uri: uri.clone(),
|
||||
path: "subject".to_string(),
|
||||
link_to: subject.to_string(),
|
||||
}])
|
||||
.map(|subject| {
|
||||
vec![Backlink {
|
||||
uri: uri.clone(),
|
||||
path: "subject".to_string(),
|
||||
link_to: subject.to_string(),
|
||||
}]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
"app.bsky.feed.like" | "app.bsky.feed.repost" => record
|
||||
.get("subject")
|
||||
.and_then(|v| v.get("uri"))
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| s.starts_with("at://"))
|
||||
.map(|subject_uri| vec![Backlink {
|
||||
uri: uri.clone(),
|
||||
path: "subject.uri".to_string(),
|
||||
link_to: subject_uri.to_string(),
|
||||
}])
|
||||
.map(|subject_uri| {
|
||||
vec![Backlink {
|
||||
uri: uri.clone(),
|
||||
path: "subject.uri".to_string(),
|
||||
link_to: subject_uri.to_string(),
|
||||
}]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
@@ -78,10 +82,10 @@ pub fn create_signed_commit(
|
||||
prev: Option<Cid>,
|
||||
signing_key: &SigningKey,
|
||||
) -> Result<(Vec<u8>, Bytes), String> {
|
||||
let did = jacquard::types::string::Did::new(did.as_str())
|
||||
let did = jacquard_common::types::string::Did::new(did.as_str())
|
||||
.map_err(|e| format!("Invalid DID: {:?}", e))?;
|
||||
let rev =
|
||||
jacquard::types::string::Tid::from_str(rev).map_err(|e| format!("Invalid TID: {:?}", e))?;
|
||||
let rev = jacquard_common::types::string::Tid::from_str(rev)
|
||||
.map_err(|e| format!("Invalid TID: {:?}", e))?;
|
||||
let unsigned = Commit::new_unsigned(did, data, rev, prev);
|
||||
let signed = unsigned
|
||||
.sign(signing_key)
|
||||
@@ -133,7 +137,9 @@ pub async fn commit_and_log(
|
||||
state: &AppState,
|
||||
params: CommitParams<'_>,
|
||||
) -> Result<CommitResult, String> {
|
||||
use tranquil_db_traits::{ApplyCommitError, ApplyCommitInput, CommitEventData, RecordDelete, RecordUpsert};
|
||||
use tranquil_db_traits::{
|
||||
ApplyCommitError, ApplyCommitInput, CommitEventData, RecordDelete, RecordUpsert,
|
||||
};
|
||||
|
||||
let CommitParams {
|
||||
did,
|
||||
@@ -179,15 +185,26 @@ pub async fn commit_and_log(
|
||||
(Vec::new(), Vec::new()),
|
||||
|(mut upserts, mut deletes), op| {
|
||||
match op {
|
||||
RecordOp::Create { collection, rkey, cid }
|
||||
| RecordOp::Update { collection, rkey, cid, .. } => {
|
||||
RecordOp::Create {
|
||||
collection,
|
||||
rkey,
|
||||
cid,
|
||||
}
|
||||
| RecordOp::Update {
|
||||
collection,
|
||||
rkey,
|
||||
cid,
|
||||
..
|
||||
} => {
|
||||
upserts.push(RecordUpsert {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
cid: crate::types::CidLink::new_unchecked(&cid.to_string()),
|
||||
cid: crate::types::CidLink::new_unchecked(cid.to_string()),
|
||||
});
|
||||
}
|
||||
RecordOp::Delete { collection, rkey, .. } => {
|
||||
RecordOp::Delete {
|
||||
collection, rkey, ..
|
||||
} => {
|
||||
deletes.push(RecordDelete {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
@@ -201,12 +218,21 @@ pub async fn commit_and_log(
|
||||
let ops_json: Vec<serde_json::Value> = ops
|
||||
.iter()
|
||||
.map(|op| match op {
|
||||
RecordOp::Create { collection, rkey, cid } => json!({
|
||||
RecordOp::Create {
|
||||
collection,
|
||||
rkey,
|
||||
cid,
|
||||
} => json!({
|
||||
"action": "create",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": cid.to_string()
|
||||
}),
|
||||
RecordOp::Update { collection, rkey, cid, prev } => {
|
||||
RecordOp::Update {
|
||||
collection,
|
||||
rkey,
|
||||
cid,
|
||||
prev,
|
||||
} => {
|
||||
let mut obj = json!({
|
||||
"action": "update",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
@@ -217,7 +243,11 @@ pub async fn commit_and_log(
|
||||
}
|
||||
obj
|
||||
}
|
||||
RecordOp::Delete { collection, rkey, prev } => {
|
||||
RecordOp::Delete {
|
||||
collection,
|
||||
rkey,
|
||||
prev,
|
||||
} => {
|
||||
let mut obj = json!({
|
||||
"action": "delete",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
@@ -234,20 +264,23 @@ pub async fn commit_and_log(
|
||||
let commit_event = CommitEventData {
|
||||
did: did.clone(),
|
||||
event_type: "commit".to_string(),
|
||||
commit_cid: Some(crate::types::CidLink::new_unchecked(&new_root_cid.to_string())),
|
||||
prev_cid: current_root_cid.map(|c| crate::types::CidLink::new_unchecked(&c.to_string())),
|
||||
commit_cid: Some(crate::types::CidLink::new_unchecked(
|
||||
new_root_cid.to_string(),
|
||||
)),
|
||||
prev_cid: current_root_cid.map(|c| crate::types::CidLink::new_unchecked(c.to_string())),
|
||||
ops: Some(json!(ops_json)),
|
||||
blobs: Some(blobs.to_vec()),
|
||||
blocks_cids: Some(blocks_cids.to_vec()),
|
||||
prev_data_cid: prev_data_cid.map(|c| crate::types::CidLink::new_unchecked(&c.to_string())),
|
||||
prev_data_cid: prev_data_cid.map(|c| crate::types::CidLink::new_unchecked(c.to_string())),
|
||||
rev: Some(rev_str.clone()),
|
||||
};
|
||||
|
||||
let input = ApplyCommitInput {
|
||||
user_id,
|
||||
did: did.clone(),
|
||||
expected_root_cid: current_root_cid.map(|c| crate::types::CidLink::new_unchecked(&c.to_string())),
|
||||
new_root_cid: crate::types::CidLink::new_unchecked(&new_root_cid.to_string()),
|
||||
expected_root_cid: current_root_cid
|
||||
.map(|c| crate::types::CidLink::new_unchecked(c.to_string())),
|
||||
new_root_cid: crate::types::CidLink::new_unchecked(new_root_cid.to_string()),
|
||||
new_rev: rev_str.clone(),
|
||||
new_block_cids: all_block_cids,
|
||||
obsolete_block_cids: obsolete_bytes,
|
||||
@@ -424,8 +457,8 @@ pub async fn sequence_genesis_commit(
|
||||
mst_root_cid: &Cid,
|
||||
rev: &str,
|
||||
) -> Result<i64, String> {
|
||||
let commit_cid_link = crate::types::CidLink::new_unchecked(&commit_cid.to_string());
|
||||
let mst_root_cid_link = crate::types::CidLink::new_unchecked(&mst_root_cid.to_string());
|
||||
let commit_cid_link = crate::types::CidLink::new_unchecked(commit_cid.to_string());
|
||||
let mst_root_cid_link = crate::types::CidLink::new_unchecked(mst_root_cid.to_string());
|
||||
state
|
||||
.repo_repo
|
||||
.insert_genesis_commit_event(did, &commit_cid_link, &mst_root_cid_link, rev)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::validation::validate_record_with_status;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids};
|
||||
use crate::api::repo::record::utils::{
|
||||
CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids,
|
||||
};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
@@ -100,7 +102,9 @@ pub async fn prepare_repo_write(
|
||||
error!("DB error fetching repo root: {}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
})?
|
||||
.ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())).into_response())?;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InternalError(Some("Repo root not found".into())).into_response()
|
||||
})?;
|
||||
let current_root_cid = Cid::from_str(&root_cid_str).map_err(|_| {
|
||||
ApiError::InternalError(Some("Invalid repo root CID".into())).into_response()
|
||||
})?;
|
||||
@@ -245,14 +249,21 @@ pub async fn create_record(
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if mst.blocks_for_path(&conflict_key, &mut all_old_mst_blocks).await.is_err() {
|
||||
if mst
|
||||
.blocks_for_path(&conflict_key, &mut all_old_mst_blocks)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
error!("Failed to get old MST blocks for conflict {}", conflict_uri);
|
||||
}
|
||||
|
||||
mst = match mst.delete(&conflict_key).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to delete conflict from MST {}: {:?}", conflict_uri, e);
|
||||
error!(
|
||||
"Failed to delete conflict from MST {}: {:?}",
|
||||
conflict_uri, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -281,7 +292,11 @@ pub async fn create_record(
|
||||
};
|
||||
let key = format!("{}/{}", input.collection, rkey);
|
||||
|
||||
if mst.blocks_for_path(&key, &mut all_old_mst_blocks).await.is_err() {
|
||||
if mst
|
||||
.blocks_for_path(&key, &mut all_old_mst_blocks)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
error!("Failed to get old MST blocks for new record path");
|
||||
}
|
||||
|
||||
@@ -355,7 +370,11 @@ pub async fn create_record(
|
||||
};
|
||||
|
||||
for conflict_uri in conflict_uris_to_cleanup {
|
||||
if let Err(e) = state.backlink_repo.remove_backlinks_by_uri(&conflict_uri).await {
|
||||
if let Err(e) = state
|
||||
.backlink_repo
|
||||
.remove_backlinks_by_uri(&conflict_uri)
|
||||
.await
|
||||
{
|
||||
error!("Failed to remove backlinks for {}: {}", conflict_uri, e);
|
||||
}
|
||||
}
|
||||
@@ -381,14 +400,10 @@ pub async fn create_record(
|
||||
|
||||
let created_uri = AtUri::from_parts(&did, &input.collection, &rkey);
|
||||
let backlinks = extract_backlinks(&created_uri, &input.record);
|
||||
if !backlinks.is_empty() {
|
||||
if let Err(e) = state
|
||||
.backlink_repo
|
||||
.add_backlinks(user_id, &backlinks)
|
||||
.await
|
||||
{
|
||||
error!("Failed to add backlinks for {}: {}", created_uri, e);
|
||||
}
|
||||
if !backlinks.is_empty()
|
||||
&& let Err(e) = state.backlink_repo.add_backlinks(user_id, &backlinks).await
|
||||
{
|
||||
error!("Failed to add backlinks for {}: {}", created_uri, e);
|
||||
}
|
||||
|
||||
(
|
||||
|
||||
@@ -110,13 +110,18 @@ pub async fn check_account_status(
|
||||
String::new()
|
||||
};
|
||||
let record_count: i64 = state.repo_repo.count_records(user_id).await.unwrap_or(0);
|
||||
let imported_blobs: i64 = state.blob_repo.count_blobs_by_user(user_id).await.unwrap_or(0);
|
||||
let imported_blobs: i64 = state
|
||||
.blob_repo
|
||||
.count_blobs_by_user(user_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let expected_blobs: i64 = state
|
||||
.blob_repo
|
||||
.count_distinct_record_blobs(user_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let valid_did = is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await;
|
||||
let valid_did =
|
||||
is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await;
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(CheckAccountStatusOutput {
|
||||
@@ -134,7 +139,11 @@ pub async fn check_account_status(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn is_valid_did_for_service(user_repo: &dyn tranquil_db_traits::UserRepository, cache: Arc<dyn Cache>, did: &crate::types::Did) -> bool {
|
||||
async fn is_valid_did_for_service(
|
||||
user_repo: &dyn tranquil_db_traits::UserRepository,
|
||||
cache: Arc<dyn Cache>,
|
||||
did: &crate::types::Did,
|
||||
) -> bool {
|
||||
assert_valid_did_document_for_service(user_repo, cache, did, false)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -235,20 +244,18 @@ async fn assert_valid_did_document_for_service(
|
||||
.and_then(|v| v.get("atproto"))
|
||||
.and_then(|k| k.as_str());
|
||||
|
||||
let user_key = user_repo
|
||||
.get_user_key_by_did(&did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to fetch user key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let user_key = user_repo.get_user_key_by_did(did).await.map_err(|e| {
|
||||
error!("Failed to fetch user key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Some(key_info) = user_key {
|
||||
let key_bytes = crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt user key: {}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let key_bytes =
|
||||
crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt user key: {}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| {
|
||||
error!("Failed to create signing key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
@@ -382,9 +389,13 @@ pub async fn activate_account(
|
||||
did
|
||||
);
|
||||
let did_validation_start = std::time::Instant::now();
|
||||
if let Err(e) =
|
||||
assert_valid_did_document_for_service(state.user_repo.as_ref(), state.cache.clone(), &did, true)
|
||||
.await
|
||||
if let Err(e) = assert_valid_did_document_for_service(
|
||||
state.user_repo.as_ref(),
|
||||
state.cache.clone(),
|
||||
&did,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: DID document validation FAILED for {} (took {:?})",
|
||||
@@ -399,12 +410,7 @@ pub async fn activate_account(
|
||||
did_validation_start.elapsed()
|
||||
);
|
||||
|
||||
let handle = state
|
||||
.user_repo
|
||||
.get_handle_by_did(&did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten();
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: Activating account did={} handle={:?}",
|
||||
did, handle
|
||||
@@ -562,17 +568,9 @@ pub async fn deactivate_account(
|
||||
|
||||
let did = auth_user.did;
|
||||
|
||||
let handle = state
|
||||
.user_repo
|
||||
.get_handle_by_did(&did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten();
|
||||
|
||||
let result = state
|
||||
.user_repo
|
||||
.deactivate_account(&did, delete_after)
|
||||
.await;
|
||||
let result = state.user_repo.deactivate_account(&did, delete_after).await;
|
||||
|
||||
match result {
|
||||
Ok(true) => {
|
||||
@@ -591,9 +589,7 @@ pub async fn deactivate_account(
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
}
|
||||
Ok(false) => {
|
||||
EmptyResponse::ok().into_response()
|
||||
}
|
||||
Ok(false) => EmptyResponse::ok().into_response(),
|
||||
Err(e) => {
|
||||
error!("DB error deactivating account: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
@@ -635,8 +631,12 @@ pub async fn request_account_delete(
|
||||
let did = validated.did.clone();
|
||||
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &did).await {
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &did)
|
||||
.await;
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let user_id = match state.user_repo.get_id_by_did(&did).await {
|
||||
@@ -742,28 +742,15 @@ pub async fn delete_account(
|
||||
let _ = state.infra_repo.delete_deletion_request(token).await;
|
||||
return ApiError::ExpiredToken(None).into_response();
|
||||
}
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.delete_account_complete(user_id, did)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = state.user_repo.delete_account_complete(user_id, did).await {
|
||||
error!("DB error deleting account: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let account_seq = crate::api::repo::record::sequence_account_event(
|
||||
&state,
|
||||
did,
|
||||
false,
|
||||
Some("deleted"),
|
||||
)
|
||||
.await;
|
||||
let account_seq =
|
||||
crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await;
|
||||
match account_seq {
|
||||
Ok(seq) => {
|
||||
if let Err(e) = state
|
||||
.repo_repo
|
||||
.delete_sequences_except(did, seq)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = state.repo_repo.delete_sequences_except(did, seq).await {
|
||||
warn!(
|
||||
"Failed to cleanup sequences for deleted account {}: {}",
|
||||
did, e
|
||||
|
||||
@@ -11,8 +11,8 @@ use axum::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tranquil_db_traits::AppPasswordCreate;
|
||||
use tracing::{error, warn};
|
||||
use tranquil_db_traits::AppPasswordCreate;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -53,7 +53,10 @@ pub async fn list_app_passwords(
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
privileged: row.privileged,
|
||||
scopes: row.scopes.clone(),
|
||||
created_by_controller: row.created_by_controller_did.as_ref().map(|d| d.to_string()),
|
||||
created_by_controller: row
|
||||
.created_by_controller_did
|
||||
.as_ref()
|
||||
.map(|d| d.to_string()),
|
||||
})
|
||||
.collect();
|
||||
Json(ListAppPasswordsOutput { passwords }).into_response()
|
||||
@@ -112,7 +115,11 @@ pub async fn create_app_password(
|
||||
return ApiError::InvalidRequest("name is required".into()).into_response();
|
||||
}
|
||||
|
||||
match state.session_repo.get_app_password_by_name(user.id, name).await {
|
||||
match state
|
||||
.session_repo
|
||||
.get_app_password_by_name(user.id, name)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(),
|
||||
Err(e) => {
|
||||
error!("DB error checking app password: {:?}", e);
|
||||
|
||||
@@ -254,7 +254,11 @@ pub async fn update_email(
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(true) = state.user_repo.check_email_exists(&new_email, user_id).await {
|
||||
if let Ok(true) = state
|
||||
.user_repo
|
||||
.check_email_exists(&new_email, user_id)
|
||||
.await
|
||||
{
|
||||
return ApiError::InvalidRequest("Email is already in use".into()).into_response();
|
||||
}
|
||||
|
||||
@@ -264,7 +268,7 @@ pub async fn update_email(
|
||||
}
|
||||
|
||||
let verification_token =
|
||||
crate::auth::verification_token::generate_signup_token(&did, "email", &new_email);
|
||||
crate::auth::verification_token::generate_signup_token(did, "email", &new_email);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
|
||||
@@ -137,7 +137,10 @@ pub async fn create_invite_codes(
|
||||
infra_repo
|
||||
.create_invite_codes_batch(&codes, use_count, admin_user_id, Some(&account))
|
||||
.await
|
||||
.map(|_| AccountCodes { account: account.to_string(), codes })
|
||||
.map(|_| AccountCodes {
|
||||
account: account.to_string(),
|
||||
codes,
|
||||
})
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
@@ -239,7 +242,10 @@ pub async fn get_account_invite_codes(
|
||||
available: info.available_uses,
|
||||
disabled: false,
|
||||
for_account: info.for_account.map(|d| d.to_string()).unwrap_or_default(),
|
||||
created_by: info.created_by.map(|d| d.to_string()).unwrap_or_else(|| "admin".to_string()),
|
||||
created_by: info
|
||||
.created_by
|
||||
.map(|d| d.to_string())
|
||||
.unwrap_or_else(|| "admin".to_string()),
|
||||
created_at: info.created_at.to_rfc3339(),
|
||||
uses,
|
||||
})
|
||||
|
||||
@@ -266,7 +266,12 @@ async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_
|
||||
});
|
||||
}
|
||||
|
||||
let key_info = state.user_repo.get_user_key_by_id(user.id).await.ok().flatten();
|
||||
let key_info = state
|
||||
.user_repo
|
||||
.get_user_key_by_id(user.id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let public_key_multibase = match key_info {
|
||||
Some(info) => match crate::config::decrypt_key(&info.key_bytes, info.encryption_version) {
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use chrono::{Duration, Utc};
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -183,7 +183,11 @@ pub async fn create_passkey_account(
|
||||
}
|
||||
|
||||
if let Some(ref code) = input.invite_code {
|
||||
let valid = state.infra_repo.is_invite_code_valid(code).await.unwrap_or(false);
|
||||
let valid = state
|
||||
.infra_repo
|
||||
.is_invite_code_valid(code)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if !valid {
|
||||
return ApiError::InvalidInviteCode.into_response();
|
||||
@@ -226,7 +230,11 @@ pub async fn create_passkey_account(
|
||||
|
||||
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<Uuid>) =
|
||||
if let Some(signing_key_did) = &input.signing_key {
|
||||
match state.infra_repo.get_reserved_signing_key(signing_key_did).await {
|
||||
match state
|
||||
.infra_repo
|
||||
.get_reserved_signing_key(signing_key_did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(reserved)) => (reserved.private_key_bytes, Some(reserved.id)),
|
||||
Ok(None) => {
|
||||
return ApiError::InvalidSigningKey.into_response();
|
||||
@@ -433,9 +441,24 @@ pub async fn create_passkey_account(
|
||||
email: email.clone().unwrap_or_default(),
|
||||
did: did_typed.clone(),
|
||||
preferred_comms_channel,
|
||||
discord_id: input.discord_id.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from),
|
||||
telegram_username: input.telegram_username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from),
|
||||
signal_number: input.signal_number.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()).map(String::from),
|
||||
discord_id: input
|
||||
.discord_id
|
||||
.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
telegram_username: input
|
||||
.telegram_username
|
||||
.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
signal_number: input
|
||||
.signal_number
|
||||
.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
setup_token_hash,
|
||||
setup_expires_at,
|
||||
deactivated_at,
|
||||
@@ -715,7 +738,7 @@ pub async fn complete_passkey_setup(
|
||||
|
||||
Json(CompletePasskeySetupResponse {
|
||||
did: input.did.clone(),
|
||||
handle: user.handle.into(),
|
||||
handle: user.handle,
|
||||
app_password,
|
||||
app_password_name,
|
||||
})
|
||||
@@ -851,7 +874,11 @@ pub async fn request_passkey_recovery(
|
||||
format!("{}.{}", identifier, hostname_for_handles)
|
||||
};
|
||||
|
||||
let user = match state.user_repo.get_user_for_passkey_recovery(identifier, &normalized_handle).await {
|
||||
let user = match state
|
||||
.user_repo
|
||||
.get_user_for_passkey_recovery(identifier, &normalized_handle)
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) if !u.password_required => u,
|
||||
_ => {
|
||||
return SuccessResponse::ok().into_response();
|
||||
@@ -867,7 +894,11 @@ pub async fn request_passkey_recovery(
|
||||
};
|
||||
let expires_at = Utc::now() + Duration::hours(1);
|
||||
|
||||
if let Err(e) = state.user_repo.set_recovery_token(&user.did, &recovery_token_hash, expires_at).await {
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.set_recovery_token(&user.did, &recovery_token_hash, expires_at)
|
||||
.await
|
||||
{
|
||||
error!("Error updating recovery token: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
@@ -944,7 +975,11 @@ pub async fn recover_passkey_account(
|
||||
did: input.did.clone(),
|
||||
password_hash,
|
||||
};
|
||||
let result = match state.user_repo.recover_passkey_account(&recover_input).await {
|
||||
let result = match state
|
||||
.user_repo
|
||||
.recover_passkey_account(&recover_input)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error recovering passkey account: {:?}", e);
|
||||
|
||||
@@ -269,13 +269,24 @@ pub async fn delete_passkey(
|
||||
auth: BearerAuth,
|
||||
Json(input): Json<DeletePasskeyInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did)
|
||||
.await;
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await;
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let id: uuid::Uuid = match input.id.parse() {
|
||||
|
||||
@@ -67,7 +67,7 @@ pub async fn request_password_reset(
|
||||
};
|
||||
let user_id = match state
|
||||
.user_repo
|
||||
.get_id_by_email_or_handle(&normalized, &normalized_handle)
|
||||
.get_id_by_email_or_handle(normalized, &normalized_handle)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
@@ -209,9 +209,15 @@ pub async fn change_password(
|
||||
auth: BearerAuth,
|
||||
Json(input): Json<ChangePasswordInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did)
|
||||
.await;
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let current_password = &input.current_password;
|
||||
@@ -225,7 +231,11 @@ pub async fn change_password(
|
||||
if let Err(e) = validate_password(new_password) {
|
||||
return ApiError::InvalidRequest(e.to_string()).into_response();
|
||||
}
|
||||
let user = match state.user_repo.get_id_and_password_hash_by_did(&auth.0.did).await {
|
||||
let user = match state
|
||||
.user_repo
|
||||
.get_id_and_password_hash_by_did(&auth.0.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
@@ -259,7 +269,11 @@ pub async fn change_password(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
if let Err(e) = state.user_repo.update_password_hash(user_id, &new_hash).await {
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.update_password_hash(user_id, &new_hash)
|
||||
.await
|
||||
{
|
||||
error!("DB error updating password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
@@ -279,9 +293,15 @@ pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth
|
||||
}
|
||||
|
||||
pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did)
|
||||
.await;
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if crate::api::server::reauth::check_reauth_required_cached(
|
||||
@@ -291,10 +311,19 @@ pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) ->
|
||||
)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await;
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let has_passkeys = state.user_repo.has_passkeys(&auth.0.did).await.unwrap_or(false);
|
||||
let has_passkeys = state
|
||||
.user_repo
|
||||
.has_passkeys(&auth.0.did)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if !has_passkeys {
|
||||
return ApiError::InvalidRequest(
|
||||
"You must have at least one passkey registered before removing your password".into(),
|
||||
@@ -344,7 +373,12 @@ pub async fn set_password(
|
||||
)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await;
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let new_password = &input.new_password;
|
||||
@@ -387,7 +421,11 @@ pub async fn set_password(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = state.user_repo.set_new_user_password(user.id, &new_hash).await {
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.set_new_user_password(user.id, &new_hash)
|
||||
.await
|
||||
{
|
||||
error!("DB error setting password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
|
||||
@@ -372,7 +372,10 @@ async fn get_available_reauth_methods(
|
||||
methods
|
||||
}
|
||||
|
||||
pub async fn check_reauth_required(session_repo: &dyn SessionRepository, did: &crate::types::Did) -> bool {
|
||||
pub async fn check_reauth_required(
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
) -> bool {
|
||||
match session_repo.get_last_reauth_at(did).await {
|
||||
Ok(last_reauth_at) => is_reauth_required(last_reauth_at),
|
||||
_ => true,
|
||||
@@ -427,7 +430,10 @@ pub async fn reauth_required_response(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn check_legacy_session_mfa(session_repo: &dyn SessionRepository, did: &crate::types::Did) -> bool {
|
||||
pub async fn check_legacy_session_mfa(
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
) -> bool {
|
||||
match session_repo.get_session_mfa_status(did).await {
|
||||
Ok(Some(status)) => {
|
||||
if !status.legacy_login {
|
||||
|
||||
@@ -119,7 +119,9 @@ pub async fn get_service_auth(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match crate::auth::validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &token).await {
|
||||
match crate::auth::validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &token)
|
||||
.await
|
||||
{
|
||||
Ok(user) => user,
|
||||
Err(e) => {
|
||||
warn!(error = ?e, "getServiceAuth auth validation failed");
|
||||
|
||||
@@ -267,7 +267,7 @@ pub async fn create_session(
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
handle: handle.into(),
|
||||
did: row.did.into(),
|
||||
did: row.did,
|
||||
did_doc,
|
||||
email: row.email,
|
||||
email_confirmed: Some(row.email_verified),
|
||||
@@ -292,7 +292,8 @@ pub async fn get_session(
|
||||
);
|
||||
match db_result {
|
||||
Ok(Some(row)) => {
|
||||
let (preferred_channel, preferred_channel_verified) = match row.preferred_comms_channel {
|
||||
let (preferred_channel, preferred_channel_verified) = match row.preferred_comms_channel
|
||||
{
|
||||
tranquil_db_traits::CommsChannel::Email => ("email", row.email_verified),
|
||||
tranquil_db_traits::CommsChannel::Discord => ("discord", row.discord_verified),
|
||||
tranquil_db_traits::CommsChannel::Telegram => ("telegram", row.telegram_verified),
|
||||
@@ -427,14 +428,16 @@ pub async fn refresh_session(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes =
|
||||
match crate::config::decrypt_key(&session_row.key_bytes, Some(session_row.encryption_version)) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt user key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes = match crate::config::decrypt_key(
|
||||
&session_row.key_bytes,
|
||||
Some(session_row.encryption_version),
|
||||
) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt user key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() {
|
||||
return ApiError::AuthenticationFailed(Some("Invalid refresh token".into()))
|
||||
.into_response();
|
||||
@@ -482,7 +485,10 @@ pub async fn refresh_session(
|
||||
.into_response();
|
||||
}
|
||||
Ok(tranquil_db_traits::RefreshSessionResult::ConcurrentRefresh) => {
|
||||
warn!("Concurrent refresh detected for session_id: {}", session_row.id);
|
||||
warn!(
|
||||
"Concurrent refresh detected for session_id: {}",
|
||||
session_row.id
|
||||
);
|
||||
return ApiError::AuthenticationFailed(Some(
|
||||
"Refresh token has been revoked due to suspected compromise".into(),
|
||||
))
|
||||
@@ -569,11 +575,7 @@ pub async fn confirm_signup(
|
||||
Json(input): Json<ConfirmSignupInput>,
|
||||
) -> Response {
|
||||
info!("confirm_signup called for DID: {}", input.did);
|
||||
let row = match state
|
||||
.user_repo
|
||||
.get_confirm_signup_by_did(&input.did)
|
||||
.await
|
||||
{
|
||||
let row = match state.user_repo.get_confirm_signup_by_did(&input.did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
warn!("User not found for confirm_signup: {}", input.did);
|
||||
@@ -653,7 +655,7 @@ pub async fn confirm_signup(
|
||||
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.set_channel_verified(&input.did, row.channel.clone())
|
||||
.set_channel_verified(&input.did, row.channel)
|
||||
.await
|
||||
{
|
||||
error!("Failed to update verification status: {:?}", e);
|
||||
@@ -698,8 +700,8 @@ pub async fn confirm_signup(
|
||||
Json(ConfirmSignupOutput {
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
handle: row.handle.into(),
|
||||
did: row.did.into(),
|
||||
handle: row.handle,
|
||||
did: row.did,
|
||||
email: row.email,
|
||||
email_verified,
|
||||
preferred_channel: preferred_channel.to_string(),
|
||||
@@ -830,7 +832,7 @@ pub async fn list_sessions(
|
||||
let is_oauth = auth.0.is_oauth;
|
||||
let oauth_sessions = oauth_rows.into_iter().map(|row| {
|
||||
let client_name = extract_client_name(&row.client_id);
|
||||
let is_current_oauth = is_oauth && current_jti.as_ref().map(|s| s.as_str()) == Some(row.token_id.as_str());
|
||||
let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str());
|
||||
SessionInfo {
|
||||
id: format!("oauth:{}", row.id),
|
||||
session_type: "oauth".to_string(),
|
||||
@@ -1003,13 +1005,24 @@ pub async fn update_legacy_login_preference(
|
||||
auth: BearerAuth,
|
||||
Json(input): Json<UpdateLegacyLoginInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did)
|
||||
.await;
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::reauth_required_response(&*state.user_repo, &*state.session_repo, &auth.0.did).await;
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
match state
|
||||
|
||||
@@ -52,12 +52,7 @@ pub async fn reserve_signing_key(
|
||||
let private_bytes: &[u8] = &private_key_bytes;
|
||||
match state
|
||||
.infra_repo
|
||||
.reserve_signing_key(
|
||||
did.as_ref(),
|
||||
&public_key_did_key,
|
||||
private_bytes,
|
||||
expires_at,
|
||||
)
|
||||
.reserve_signing_key(did.as_ref(), &public_key_did_key, private_bytes, expires_at)
|
||||
.await
|
||||
{
|
||||
Ok(key_id) => {
|
||||
|
||||
@@ -125,14 +125,16 @@ pub async fn enable_totp(
|
||||
return ApiError::TotpAlreadyEnabled.into_response();
|
||||
}
|
||||
|
||||
let secret =
|
||||
match decrypt_totp_secret(&totp_record.secret_encrypted, totp_record.encryption_version) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let secret = match decrypt_totp_secret(
|
||||
&totp_record.secret_encrypted,
|
||||
totp_record.encryption_version,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let code = input.code.trim();
|
||||
if !verify_totp_code(&secret, code) {
|
||||
@@ -175,7 +177,8 @@ pub async fn disable_totp(
|
||||
auth: BearerAuth,
|
||||
Json(input): Json<DisableTotpInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did).await
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
@@ -333,14 +336,16 @@ pub async fn regenerate_backup_codes(
|
||||
}
|
||||
};
|
||||
|
||||
let secret =
|
||||
match decrypt_totp_secret(&totp_record.secret_encrypted, totp_record.encryption_version) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let secret = match decrypt_totp_secret(
|
||||
&totp_record.secret_encrypted,
|
||||
totp_record.encryption_version,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let code = input.code.trim();
|
||||
if !verify_totp_code(&secret, code) {
|
||||
@@ -372,7 +377,11 @@ pub async fn regenerate_backup_codes(
|
||||
Json(RegenerateBackupCodesResponse { backup_codes }).into_response()
|
||||
}
|
||||
|
||||
async fn verify_backup_code_for_user(state: &AppState, did: &crate::types::Did, code: &str) -> bool {
|
||||
async fn verify_backup_code_for_user(
|
||||
state: &AppState,
|
||||
did: &crate::types::Did,
|
||||
code: &str,
|
||||
) -> bool {
|
||||
let code = code.trim().to_uppercase();
|
||||
|
||||
let backup_codes = match state.user_repo.get_unused_backup_codes(did).await {
|
||||
@@ -396,7 +405,11 @@ async fn verify_backup_code_for_user(state: &AppState, did: &crate::types::Did,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_totp_or_backup_for_user(state: &AppState, did: &crate::types::Did, code: &str) -> bool {
|
||||
pub async fn verify_totp_or_backup_for_user(
|
||||
state: &AppState,
|
||||
did: &crate::types::Did,
|
||||
code: &str,
|
||||
) -> bool {
|
||||
let code = code.trim();
|
||||
|
||||
if is_backup_code_format(code) {
|
||||
@@ -408,11 +421,13 @@ pub async fn verify_totp_or_backup_for_user(state: &AppState, did: &crate::types
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let secret =
|
||||
match decrypt_totp_secret(&totp_record.secret_encrypted, totp_record.encryption_version) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let secret = match decrypt_totp_secret(
|
||||
&totp_record.secret_encrypted,
|
||||
totp_record.encryption_version,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if verify_totp_code(&secret, code) {
|
||||
let _ = state.user_repo.update_totp_last_used(did).await;
|
||||
@@ -423,9 +438,5 @@ pub async fn verify_totp_or_backup_for_user(state: &AppState, did: &crate::types
|
||||
}
|
||||
|
||||
pub async fn has_totp_enabled(state: &AppState, did: &crate::types::Did) -> bool {
|
||||
state
|
||||
.user_repo
|
||||
.has_totp_enabled(did)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
state.user_repo.has_totp_enabled(did).await.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -188,7 +188,10 @@ pub async fn get_device_trust_state(
|
||||
did: &tranquil_types::Did,
|
||||
) -> DeviceTrustState {
|
||||
let device_id_typed = DeviceId::from(device_id.to_string());
|
||||
match oauth_repo.get_device_trust_info(&device_id_typed, did).await {
|
||||
match oauth_repo
|
||||
.get_device_trust_info(&device_id_typed, did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(info)) => DeviceTrustState::from_timestamps(info.trusted_at, info.trusted_until),
|
||||
_ => DeviceTrustState::Untrusted,
|
||||
}
|
||||
@@ -211,7 +214,9 @@ pub async fn trust_device(
|
||||
let now = Utc::now();
|
||||
let trusted_until = now + Duration::days(TRUST_DURATION_DAYS);
|
||||
let device_id_typed = DeviceId::from(device_id.to_string());
|
||||
oauth_repo.trust_device(&device_id_typed, now, trusted_until).await
|
||||
oauth_repo
|
||||
.trust_device(&device_id_typed, now, trusted_until)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn extend_device_trust(
|
||||
@@ -220,5 +225,7 @@ pub async fn extend_device_trust(
|
||||
) -> Result<(), tranquil_db_traits::DbError> {
|
||||
let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS);
|
||||
let device_id_typed = DeviceId::from(device_id.to_string());
|
||||
oauth_repo.extend_device_trust(&device_id_typed, trusted_until).await
|
||||
oauth_repo
|
||||
.extend_device_trust(&device_id_typed, trusted_until)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ async fn handle_migration_verification(
|
||||
return Err(ApiError::InvalidChannel);
|
||||
}
|
||||
|
||||
let did_typed: Did = did.parse().map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
let did_typed: Did = did
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_verification_info(&did_typed)
|
||||
@@ -116,7 +118,9 @@ async fn handle_channel_update(
|
||||
channel: &str,
|
||||
identifier: &str,
|
||||
) -> Result<Json<VerifyTokenOutput>, ApiError> {
|
||||
let did_typed: Did = did.parse().map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
let did_typed: Did = did
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(&did_typed)
|
||||
@@ -189,7 +193,9 @@ async fn handle_signup_verification(
|
||||
channel: &str,
|
||||
_identifier: &str,
|
||||
) -> Result<Json<VerifyTokenOutput>, ApiError> {
|
||||
let did_typed: Did = did.parse().map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
let did_typed: Did = did
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_verification_info(&did_typed)
|
||||
|
||||
@@ -434,7 +434,10 @@ pub async fn validate_token_with_dpop(
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let result_did: Did = result.did.parse().map_err(|_| TokenValidationError::InvalidToken)?;
|
||||
let result_did: Did = result
|
||||
.did
|
||||
.parse()
|
||||
.map_err(|_| TokenValidationError::InvalidToken)?;
|
||||
let user_info = user_repo
|
||||
.get_user_info_by_did(&result_did)
|
||||
.await
|
||||
|
||||
@@ -71,7 +71,9 @@ impl CommsService {
|
||||
CommsType::TwoFactorCode => tranquil_db_traits::CommsType::TwoFactorCode,
|
||||
CommsType::PasskeyRecovery => tranquil_db_traits::CommsType::PasskeyRecovery,
|
||||
CommsType::LegacyLoginAlert => tranquil_db_traits::CommsType::LegacyLoginAlert,
|
||||
CommsType::MigrationVerification => tranquil_db_traits::CommsType::MigrationVerification,
|
||||
CommsType::MigrationVerification => {
|
||||
tranquil_db_traits::CommsType::MigrationVerification
|
||||
}
|
||||
CommsType::ChannelVerification => tranquil_db_traits::CommsType::ChannelVerification,
|
||||
};
|
||||
let id = self
|
||||
@@ -136,7 +138,9 @@ impl CommsService {
|
||||
|
||||
async fn fetch_pending(&self) -> Result<Vec<QueuedComms>, tranquil_db_traits::DbError> {
|
||||
let now = Utc::now();
|
||||
self.infra_repo.fetch_pending_comms(now, self.batch_size).await
|
||||
self.infra_repo
|
||||
.fetch_pending_comms(now, self.batch_size)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn process_item(&self, item: QueuedComms) {
|
||||
@@ -162,8 +166,12 @@ impl CommsService {
|
||||
tranquil_db_traits::CommsType::TwoFactorCode => CommsType::TwoFactorCode,
|
||||
tranquil_db_traits::CommsType::PasskeyRecovery => CommsType::PasskeyRecovery,
|
||||
tranquil_db_traits::CommsType::LegacyLoginAlert => CommsType::LegacyLoginAlert,
|
||||
tranquil_db_traits::CommsType::MigrationVerification => CommsType::MigrationVerification,
|
||||
tranquil_db_traits::CommsType::ChannelVerification => CommsType::ChannelVerification,
|
||||
tranquil_db_traits::CommsType::MigrationVerification => {
|
||||
CommsType::MigrationVerification
|
||||
}
|
||||
tranquil_db_traits::CommsType::ChannelVerification => {
|
||||
CommsType::ChannelVerification
|
||||
}
|
||||
},
|
||||
status: match item.status {
|
||||
tranquil_db_traits::CommsStatus::Pending => CommsStatus::Pending,
|
||||
@@ -250,7 +258,6 @@ fn channel_from_str(s: &str) -> tranquil_db_traits::CommsChannel {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub mod repo {
|
||||
use super::*;
|
||||
use tranquil_db_traits::DbError;
|
||||
@@ -261,7 +268,10 @@ pub mod repo {
|
||||
user_id: Uuid,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let body = format_message(
|
||||
strings.welcome_body,
|
||||
@@ -269,15 +279,17 @@ pub mod repo {
|
||||
);
|
||||
let subject = format_message(strings.welcome_subject, &[("hostname", hostname)]);
|
||||
let channel = channel_from_str(&prefs.preferred_channel);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::Welcome,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::Welcome,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_password_reset(
|
||||
@@ -287,7 +299,10 @@ pub mod repo {
|
||||
code: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let body = format_message(
|
||||
strings.password_reset_body,
|
||||
@@ -295,15 +310,17 @@ pub mod repo {
|
||||
);
|
||||
let subject = format_message(strings.password_reset_subject, &[("hostname", hostname)]);
|
||||
let channel = channel_from_str(&prefs.preferred_channel);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::PasswordReset,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::PasswordReset,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_email_update(
|
||||
@@ -332,15 +349,17 @@ pub mod repo {
|
||||
],
|
||||
);
|
||||
let subject = format_message(strings.email_update_subject, &[("hostname", hostname)]);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
CommsType::EmailUpdate,
|
||||
new_email,
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
CommsType::EmailUpdate,
|
||||
new_email,
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_email_update_token(
|
||||
@@ -350,7 +369,10 @@ pub mod repo {
|
||||
code: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let current_email = prefs.email.unwrap_or_default();
|
||||
let verify_page = format!("https://{}/app/verify?type=email-update", hostname);
|
||||
@@ -369,15 +391,17 @@ pub mod repo {
|
||||
],
|
||||
);
|
||||
let subject = format_message(strings.email_update_subject, &[("hostname", hostname)]);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
CommsType::EmailUpdate,
|
||||
¤t_email,
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
CommsType::EmailUpdate,
|
||||
¤t_email,
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_account_deletion(
|
||||
@@ -387,7 +411,10 @@ pub mod repo {
|
||||
code: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let body = format_message(
|
||||
strings.account_deletion_body,
|
||||
@@ -395,15 +422,17 @@ pub mod repo {
|
||||
);
|
||||
let subject = format_message(strings.account_deletion_subject, &[("hostname", hostname)]);
|
||||
let channel = channel_from_str(&prefs.preferred_channel);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::AccountDeletion,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::AccountDeletion,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_plc_operation(
|
||||
@@ -413,7 +442,10 @@ pub mod repo {
|
||||
token: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let body = format_message(
|
||||
strings.plc_operation_body,
|
||||
@@ -421,15 +453,17 @@ pub mod repo {
|
||||
);
|
||||
let subject = format_message(strings.plc_operation_subject, &[("hostname", hostname)]);
|
||||
let channel = channel_from_str(&prefs.preferred_channel);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::PlcOperation,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::PlcOperation,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_passkey_recovery(
|
||||
@@ -439,7 +473,10 @@ pub mod repo {
|
||||
recovery_url: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let body = format_message(
|
||||
strings.passkey_recovery_body,
|
||||
@@ -447,15 +484,17 @@ pub mod repo {
|
||||
);
|
||||
let subject = format_message(strings.passkey_recovery_subject, &[("hostname", hostname)]);
|
||||
let channel = channel_from_str(&prefs.preferred_channel);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::PasskeyRecovery,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::PasskeyRecovery,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_migration_verification(
|
||||
@@ -466,7 +505,10 @@ pub mod repo {
|
||||
token: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let encoded_email = urlencoding::encode(email);
|
||||
let encoded_token = urlencoding::encode(token);
|
||||
@@ -488,15 +530,17 @@ pub mod repo {
|
||||
strings.migration_verification_subject,
|
||||
&[("hostname", hostname)],
|
||||
);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
CommsType::MigrationVerification,
|
||||
email,
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
CommsType::MigrationVerification,
|
||||
email,
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_signup_verification(
|
||||
@@ -539,15 +583,17 @@ pub mod repo {
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
comms_channel,
|
||||
CommsType::EmailVerification,
|
||||
recipient,
|
||||
subject.as_deref(),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
comms_channel,
|
||||
CommsType::EmailVerification,
|
||||
recipient,
|
||||
subject.as_deref(),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_2fa_code(
|
||||
@@ -557,7 +603,10 @@ pub mod repo {
|
||||
code: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let body = format_message(
|
||||
strings.two_factor_code_body,
|
||||
@@ -565,15 +614,17 @@ pub mod repo {
|
||||
);
|
||||
let subject = format_message(strings.two_factor_code_subject, &[("hostname", hostname)]);
|
||||
let channel = channel_from_str(&prefs.preferred_channel);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::TwoFactorCode,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::TwoFactorCode,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_legacy_login(
|
||||
@@ -584,7 +635,10 @@ pub mod repo {
|
||||
client_ip: &str,
|
||||
channel: tranquil_db_traits::CommsChannel,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?;
|
||||
let prefs = user_repo
|
||||
.get_comms_prefs(user_id)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let timestamp = chrono::Utc::now()
|
||||
.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
@@ -599,14 +653,16 @@ pub mod repo {
|
||||
],
|
||||
);
|
||||
let subject = format_message(strings.legacy_login_subject, &[("hostname", hostname)]);
|
||||
infra_repo.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::LegacyLoginAlert,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
).await
|
||||
infra_repo
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
CommsType::LegacyLoginAlert,
|
||||
&prefs.email.unwrap_or_default(),
|
||||
Some(&subject),
|
||||
&body,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,6 @@ pub fn intersect_scopes(requested: &str, granted: &str) -> String {
|
||||
let granted_has_atproto = granted_set.contains("atproto");
|
||||
let requested_has_atproto = requested_set.contains("atproto");
|
||||
|
||||
if granted_has_atproto && requested_has_atproto {
|
||||
return "atproto".to_string();
|
||||
}
|
||||
|
||||
if granted_has_atproto {
|
||||
return requested_set.into_iter().collect::<Vec<_>>().join(" ");
|
||||
}
|
||||
@@ -116,16 +112,14 @@ pub fn validate_delegation_scopes(scopes: &str) -> Result<(), String> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
scopes
|
||||
.split_whitespace()
|
||||
.try_for_each(|scope| {
|
||||
let (base, _) = split_scope(scope);
|
||||
if is_valid_scope_prefix(base) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Invalid scope: {}", scope))
|
||||
}
|
||||
})
|
||||
scopes.split_whitespace().try_for_each(|scope| {
|
||||
let (base, _) = split_scope(scope);
|
||||
if is_valid_scope_prefix(base) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Invalid scope: {}", scope))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_valid_scope_prefix(base: &str) -> bool {
|
||||
|
||||
@@ -559,6 +559,10 @@ pub fn app(state: AppState) -> Router {
|
||||
get(oauth::endpoints::authorize_redirect),
|
||||
)
|
||||
.route("/delegation/auth", post(oauth::endpoints::delegation_auth))
|
||||
.route(
|
||||
"/delegation/auth-token",
|
||||
post(oauth::endpoints::delegation_auth_token),
|
||||
)
|
||||
.route(
|
||||
"/delegation/totp",
|
||||
post(oauth::endpoints::delegation_totp_verify),
|
||||
|
||||
@@ -36,7 +36,10 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let backfill_block_store = state.block_store.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::join!(
|
||||
backfill_genesis_commit_blocks(backfill_repo_repo.clone(), backfill_block_store.clone()),
|
||||
backfill_genesis_commit_blocks(
|
||||
backfill_repo_repo.clone(),
|
||||
backfill_block_store.clone()
|
||||
),
|
||||
backfill_repo_rev(backfill_repo_repo.clone(), backfill_block_store.clone()),
|
||||
backfill_user_blocks(backfill_repo_repo.clone(), backfill_block_store.clone()),
|
||||
backfill_record_blobs(backfill_repo_repo, backfill_block_store),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::super::OAuthError;
|
||||
use tranquil_db_traits::OAuthRepository;
|
||||
use tranquil_types::{Did, RefreshToken};
|
||||
pub use tranquil_db_traits::RefreshTokenLookup;
|
||||
use tranquil_types::{Did, RefreshToken};
|
||||
|
||||
pub async fn lookup_refresh_token(
|
||||
oauth_repo: &dyn OAuthRepository,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
use crate::auth::{extract_auth_token_from_header, validate_token_with_dpop};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use crate::types::PlainPassword;
|
||||
use crate::util::extract_client_ip;
|
||||
use crate::util::{build_full_url, extract_client_ip};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -446,3 +447,198 @@ pub async fn delegation_totp_verify(
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DelegationTokenAuthSubmit {
|
||||
pub request_uri: String,
|
||||
pub delegated_did: String,
|
||||
}
|
||||
|
||||
pub async fn delegation_auth_token(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(form): Json<DelegationTokenAuthSubmit>,
|
||||
) -> Response {
|
||||
let auth_header = headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
let extracted = match extract_auth_token_from_header(auth_header) {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Missing or invalid authorization header".to_string()),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let dpop_proof = headers.get("dpop").and_then(|h| h.to_str().ok());
|
||||
let uri = build_full_url("/oauth/delegation/auth-token");
|
||||
|
||||
let auth_user = match validate_token_with_dpop(
|
||||
state.user_repo.as_ref(),
|
||||
state.oauth_repo.as_ref(),
|
||||
&extracted.token,
|
||||
extracted.is_dpop,
|
||||
dpop_proof,
|
||||
"POST",
|
||||
&uri,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Invalid or expired access token".to_string()),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let controller_did = auth_user.did;
|
||||
|
||||
let delegated_did: Did = match form.delegated_did.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Invalid delegated DID".to_string()),
|
||||
})
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let request_id = RequestId::from(form.request_uri.clone());
|
||||
let request = match state
|
||||
.oauth_repo
|
||||
.get_authorization_request(&request_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => {
|
||||
return Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Authorization request not found".to_string()),
|
||||
})
|
||||
.into_response();
|
||||
}
|
||||
Err(_) => {
|
||||
return Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Server error".to_string()),
|
||||
})
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let grant = match state
|
||||
.delegation_repo
|
||||
.get_delegation(&delegated_did, &controller_did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(g)) => g,
|
||||
Ok(None) => {
|
||||
return Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("No delegation grant found for this controller".to_string()),
|
||||
})
|
||||
.into_response();
|
||||
}
|
||||
Err(_) => {
|
||||
return Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Server error".to_string()),
|
||||
})
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if state
|
||||
.oauth_repo
|
||||
.set_request_did(&request_id, &delegated_did)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Failed to update authorization request".to_string()),
|
||||
})
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if state
|
||||
.oauth_repo
|
||||
.set_controller_did(&request_id, &controller_did)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Json(DelegationAuthResponse {
|
||||
success: false,
|
||||
needs_totp: None,
|
||||
redirect_uri: None,
|
||||
error: Some("Failed to update authorization request".to_string()),
|
||||
})
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let ip = extract_client_ip(&headers);
|
||||
let user_agent = headers
|
||||
.get("user-agent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&delegated_did,
|
||||
&controller_did,
|
||||
Some(&controller_did),
|
||||
DelegationActionType::TokenIssued,
|
||||
Some(serde_json::json!({
|
||||
"client_id": request.client_id,
|
||||
"granted_scopes": grant.granted_scopes,
|
||||
"auth_method": "token"
|
||||
})),
|
||||
Some(&ip),
|
||||
user_agent.as_deref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Json(DelegationAuthResponse {
|
||||
success: true,
|
||||
needs_totp: None,
|
||||
redirect_uri: Some(format!(
|
||||
"/app/oauth/consent?request_uri={}",
|
||||
urlencoding::encode(&form.request_uri)
|
||||
)),
|
||||
error: None,
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use crate::oauth::{
|
||||
scopes::{ParsedScope, parse_scope},
|
||||
};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use tranquil_types::RequestId as RequestIdType;
|
||||
use axum::body::Bytes;
|
||||
use axum::{Json, extract::State, http::HeaderMap};
|
||||
use chrono::{Duration, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tranquil_types::RequestId as RequestIdType;
|
||||
|
||||
const PAR_EXPIRY_SECONDS: i64 = 600;
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ use crate::delegation::intersect_scopes;
|
||||
use crate::oauth::{
|
||||
AuthFlowState, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken,
|
||||
TokenData, TokenId,
|
||||
db::{lookup_refresh_token, enforce_token_limit_for_user},
|
||||
db::{enforce_token_limit_for_user, lookup_refresh_token},
|
||||
scopes::expand_include_scopes,
|
||||
verify_client_auth,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use tranquil_db_traits::RefreshTokenLookup;
|
||||
use tranquil_types::{AuthorizationCode, Did, RefreshToken as RefreshTokenType};
|
||||
use axum::Json;
|
||||
use axum::http::HeaderMap;
|
||||
use chrono::{Duration, Utc};
|
||||
use tranquil_db_traits::RefreshTokenLookup;
|
||||
use tranquil_types::{AuthorizationCode, Did, RefreshToken as RefreshTokenType};
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300;
|
||||
const REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL: i64 = 60;
|
||||
@@ -136,12 +136,12 @@ pub async fn handle_authorization_code_grant(
|
||||
let now = Utc::now();
|
||||
|
||||
let (raw_scope, controller_did) = if let Some(ref controller) = auth_request.controller_did {
|
||||
let did_parsed: Did = did.parse().map_err(|_| {
|
||||
OAuthError::InvalidRequest("Invalid DID format".to_string())
|
||||
})?;
|
||||
let controller_parsed: Did = controller.parse().map_err(|_| {
|
||||
OAuthError::InvalidRequest("Invalid controller DID format".to_string())
|
||||
})?;
|
||||
let did_parsed: Did = did
|
||||
.parse()
|
||||
.map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?;
|
||||
let controller_parsed: Did = controller
|
||||
.parse()
|
||||
.map_err(|_| OAuthError::InvalidRequest("Invalid controller DID format".to_string()))?;
|
||||
let grant = state
|
||||
.delegation_repo
|
||||
.get_delegation(&did_parsed, &controller_parsed)
|
||||
@@ -216,10 +216,10 @@ pub async fn handle_authorization_code_grant(
|
||||
let oauth_repo = state.oauth_repo.clone();
|
||||
let did_clone = did.clone();
|
||||
async move {
|
||||
if let Ok(did_typed) = did_clone.parse::<tranquil_types::Did>() {
|
||||
if let Err(e) = enforce_token_limit_for_user(oauth_repo.as_ref(), &did_typed).await {
|
||||
tracing::warn!("Failed to enforce token limit for user: {:?}", e);
|
||||
}
|
||||
if let Ok(did_typed) = did_clone.parse::<tranquil_types::Did>()
|
||||
&& let Err(e) = enforce_token_limit_for_user(oauth_repo.as_ref(), &did_typed).await
|
||||
{
|
||||
tracing::warn!("Failed to enforce token limit for user: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -385,7 +385,10 @@ struct LegacyAuthResult {
|
||||
did: String,
|
||||
}
|
||||
|
||||
async fn try_legacy_auth(user_repo: &dyn UserRepository, token: &str) -> Result<LegacyAuthResult, ()> {
|
||||
async fn try_legacy_auth(
|
||||
user_repo: &dyn UserRepository,
|
||||
token: &str,
|
||||
) -> Result<LegacyAuthResult, ()> {
|
||||
match crate::auth::validate_bearer_token(user_repo, token).await {
|
||||
Ok(user) if !user.is_oauth => Ok(LegacyAuthResult {
|
||||
did: user.did.to_string(),
|
||||
|
||||
@@ -526,11 +526,9 @@ pub fn verify_operation_signature(op: &Value, rotation_keys: &[String]) -> Resul
|
||||
}
|
||||
let cbor_bytes = serde_ipld_dagcbor::to_vec(&unsigned_op)
|
||||
.map_err(|e| PlcError::Serialization(e.to_string()))?;
|
||||
let verified = rotation_keys
|
||||
.iter()
|
||||
.any(|key_did| {
|
||||
verify_signature_with_did_key(key_did, &cbor_bytes, &signature).unwrap_or(false)
|
||||
});
|
||||
let verified = rotation_keys.iter().any(|key_did| {
|
||||
verify_signature_with_did_key(key_did, &cbor_bytes, &signature).unwrap_or(false)
|
||||
});
|
||||
Ok(verified)
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,11 @@ pub async fn backfill_genesis_commit_blocks(
|
||||
(s + 1, f)
|
||||
}
|
||||
Err((seq, reason)) => {
|
||||
warn!(seq = seq, reason = reason, "Failed to process genesis commit");
|
||||
warn!(
|
||||
seq = seq,
|
||||
reason = reason,
|
||||
"Failed to process genesis commit"
|
||||
);
|
||||
(s, f + 1)
|
||||
}
|
||||
});
|
||||
@@ -94,12 +98,7 @@ async fn process_repo_rev(
|
||||
repo_root_cid: String,
|
||||
) -> Result<uuid::Uuid, uuid::Uuid> {
|
||||
let cid = Cid::from_str(&repo_root_cid).map_err(|_| user_id)?;
|
||||
let block = block_store
|
||||
.get(&cid)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(user_id)?;
|
||||
let block = block_store.get(&cid).await.ok().flatten().ok_or(user_id)?;
|
||||
let commit = Commit::from_cbor(&block).map_err(|_| user_id)?;
|
||||
let rev = commit.rev().to_string();
|
||||
repo_repo
|
||||
@@ -135,8 +134,13 @@ pub async fn backfill_repo_rev(
|
||||
let repo_repo = repo_repo.clone();
|
||||
let block_store = block_store.clone();
|
||||
async move {
|
||||
process_repo_rev(repo_repo.as_ref(), &block_store, repo.user_id, repo.repo_root_cid.to_string())
|
||||
.await
|
||||
process_repo_rev(
|
||||
repo_repo.as_ref(),
|
||||
&block_store,
|
||||
repo.user_id,
|
||||
repo.repo_root_cid.to_string(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
@@ -304,8 +308,11 @@ async fn process_record_blobs(
|
||||
blob_refs
|
||||
.into_iter()
|
||||
.map(|blob_ref| {
|
||||
let record_uri =
|
||||
AtUri::from_parts(did.as_str(), record.collection.as_str(), record.rkey.as_str());
|
||||
let record_uri = AtUri::from_parts(
|
||||
did.as_str(),
|
||||
record.collection.as_str(),
|
||||
record.rkey.as_str(),
|
||||
);
|
||||
(record_uri, CidLink::new_unchecked(blob_ref.cid))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -335,7 +342,8 @@ pub async fn backfill_record_blobs(
|
||||
repo_repo: Arc<dyn RepoRepository>,
|
||||
block_store: PostgresBlockStore,
|
||||
) {
|
||||
let users_needing_backfill = match repo_repo.get_users_needing_record_blobs_backfill(100).await {
|
||||
let users_needing_backfill = match repo_repo.get_users_needing_record_blobs_backfill(100).await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
error!("Failed to query users for record_blobs backfill: {:?}", e);
|
||||
@@ -449,7 +457,9 @@ async fn process_scheduled_deletions(
|
||||
.into_iter()
|
||||
.for_each(|(did, handle, result)| match result {
|
||||
Ok(()) => info!(did = %did, handle = %handle, "Successfully deleted scheduled account"),
|
||||
Err(e) => warn!(did = %did, handle = %handle, error = %e, "Failed to delete scheduled account"),
|
||||
Err(e) => {
|
||||
warn!(did = %did, handle = %handle, error = %e, "Failed to delete scheduled account")
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -545,6 +555,7 @@ enum BackupOutcome {
|
||||
Failed(String, String),
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn process_single_backup(
|
||||
repo_repo: &dyn RepoRepository,
|
||||
backup_repo: &dyn BackupRepository,
|
||||
@@ -657,9 +668,13 @@ async fn process_scheduled_backups(
|
||||
block_count = result.block_count,
|
||||
"Created backup"
|
||||
);
|
||||
if let Err(e) =
|
||||
cleanup_old_backups(backup_repo, backup_storage, result.user_id, retention_count)
|
||||
.await
|
||||
if let Err(e) = cleanup_old_backups(
|
||||
backup_repo,
|
||||
backup_storage,
|
||||
result.user_id,
|
||||
retention_count,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(did = %result.did, error = %e, "Failed to cleanup old backups");
|
||||
}
|
||||
@@ -810,7 +825,10 @@ async fn cleanup_old_backups(
|
||||
match backup_storage.delete_backup(&backup.storage_key).await {
|
||||
Ok(()) => match backup_repo.delete_backup(backup.id).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(format!("DB delete failed for {}: {:?}", backup.storage_key, e)),
|
||||
Err(e) => Err(format!(
|
||||
"DB delete failed for {}: {:?}",
|
||||
backup.storage_key, e
|
||||
)),
|
||||
},
|
||||
Err(e) => {
|
||||
warn!(
|
||||
|
||||
@@ -47,22 +47,20 @@ pub async fn get_blob(
|
||||
|
||||
let blob_result = state.blob_repo.get_blob_metadata(&cid).await;
|
||||
match blob_result {
|
||||
Ok(Some(metadata)) => {
|
||||
match state.blob_store.get(&metadata.storage_key).await {
|
||||
Ok(data) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, &metadata.mime_type)
|
||||
.header(header::CONTENT_LENGTH, metadata.size_bytes.to_string())
|
||||
.header("x-content-type-options", "nosniff")
|
||||
.header("content-security-policy", "default-src 'none'; sandbox")
|
||||
.body(Body::from(data))
|
||||
.unwrap(),
|
||||
Err(e) => {
|
||||
error!("Failed to fetch blob from storage: {:?}", e);
|
||||
ApiError::BlobNotFound(Some("Blob not found in storage".into())).into_response()
|
||||
}
|
||||
Ok(Some(metadata)) => match state.blob_store.get(&metadata.storage_key).await {
|
||||
Ok(data) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, &metadata.mime_type)
|
||||
.header(header::CONTENT_LENGTH, metadata.size_bytes.to_string())
|
||||
.header("x-content-type-options", "nosniff")
|
||||
.header("content-security-policy", "default-src 'none'; sandbox")
|
||||
.body(Body::from(data))
|
||||
.unwrap(),
|
||||
Err(e) => {
|
||||
error!("Failed to fetch blob from storage: {:?}", e);
|
||||
ApiError::BlobNotFound(Some("Blob not found in storage".into())).into_response()
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(None) => ApiError::BlobNotFound(Some("Blob not found".into())).into_response(),
|
||||
Err(e) => {
|
||||
error!("DB error in get_blob: {:?}", e);
|
||||
|
||||
@@ -102,12 +102,12 @@ pub async fn list_repos(
|
||||
Query(params): Query<ListReposParams>,
|
||||
) -> Response {
|
||||
let limit = params.limit.unwrap_or(50).clamp(1, 1000);
|
||||
let cursor_did: Option<Did> = params
|
||||
.cursor
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse().ok());
|
||||
let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|s| s.parse().ok());
|
||||
let cursor_ref = cursor_did.as_ref();
|
||||
let result = state.repo_repo.list_repos_paginated(cursor_ref, limit + 1).await;
|
||||
let result = state
|
||||
.repo_repo
|
||||
.list_repos_paginated(cursor_ref, limit + 1)
|
||||
.await;
|
||||
match result {
|
||||
Ok(rows) => {
|
||||
let has_more = rows.len() as i64 > limit;
|
||||
@@ -196,8 +196,11 @@ pub async fn get_repo_status(
|
||||
let account = match get_account_with_status(state.repo_repo.as_ref(), &did).await {
|
||||
Ok(Some(a)) => a,
|
||||
Ok(None) => {
|
||||
return ApiError::RepoNotFound(Some(format!("Could not find repo for DID: {}", did_str)))
|
||||
.into_response();
|
||||
return ApiError::RepoNotFound(Some(format!(
|
||||
"Could not find repo for DID: {}",
|
||||
did_str
|
||||
)))
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in get_repo_status: {:?}", e);
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::api::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
use crate::sync::car::encode_car_header;
|
||||
use crate::sync::util::assert_repo_availability;
|
||||
use tranquil_types::Did;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -15,6 +14,7 @@ use jacquard_repo::storage::BlockStore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
use tranquil_types::Did;
|
||||
|
||||
const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000;
|
||||
|
||||
@@ -69,10 +69,11 @@ pub async fn get_head(
|
||||
Err(_) => return ApiError::InvalidRequest("invalid did".into()).into_response(),
|
||||
};
|
||||
let is_admin_or_self = check_admin_or_self(&state, &headers, did_str).await;
|
||||
let account = match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
let account =
|
||||
match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
match account.repo_root_cid {
|
||||
Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(),
|
||||
None => ApiError::RepoNotFound(Some(format!("Could not find root for DID: {}", did_str)))
|
||||
@@ -99,10 +100,11 @@ pub async fn get_checkout(
|
||||
Err(_) => return ApiError::InvalidRequest("invalid did".into()).into_response(),
|
||||
};
|
||||
let is_admin_or_self = check_admin_or_self(&state, &headers, did_str).await;
|
||||
let account = match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
let account =
|
||||
match assert_repo_availability(state.repo_repo.as_ref(), &did, is_admin_or_self).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
let Some(head_str) = account.repo_root_cid else {
|
||||
return ApiError::RepoNotFound(Some("Repo not initialized".into())).into_response();
|
||||
};
|
||||
|
||||
@@ -186,7 +186,7 @@ impl CommitFrameBuilder {
|
||||
}
|
||||
|
||||
fn placeholder_rev() -> String {
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
Tid::now(LimitedU32::MIN).to_string()
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,12 @@ async fn listen_loop(state: AppState) -> anyhow::Result<()> {
|
||||
});
|
||||
}
|
||||
}
|
||||
let event = state.repo_repo.get_event_by_seq(seq_id).await.ok().flatten();
|
||||
let event = state
|
||||
.repo_repo
|
||||
.get_event_by_seq(seq_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(event) = event {
|
||||
let seq = event.seq;
|
||||
let firehose_event = to_firehose_event(event);
|
||||
|
||||
@@ -181,7 +181,10 @@ async fn get_repo_since(state: &AppState, did: &Did, head_cid: &Cid, since: &str
|
||||
}
|
||||
};
|
||||
|
||||
let block_cid_bytes = match state.repo_repo.get_user_block_cids_since_rev(user_id, since).await
|
||||
let block_cid_bytes = match state
|
||||
.repo_repo
|
||||
.get_user_block_cids_since_rev(user_id, since)
|
||||
.await
|
||||
{
|
||||
Ok(cids) => cids,
|
||||
Err(e) => {
|
||||
|
||||
@@ -105,7 +105,12 @@ async fn handle_socket_inner(
|
||||
let _ = socket.send(Message::Binary(info_bytes.into())).await;
|
||||
}
|
||||
|
||||
let earliest = state.repo_repo.get_min_seq_since(backfill_time).await.ok().flatten();
|
||||
let earliest = state
|
||||
.repo_repo
|
||||
.get_min_seq_since(backfill_time)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some(earliest_seq) = earliest {
|
||||
current_cursor = earliest_seq - 1;
|
||||
@@ -162,10 +167,7 @@ async fn handle_socket_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let cutover_events = state
|
||||
.repo_repo
|
||||
.get_events_since_seq(last_seen, None)
|
||||
.await;
|
||||
let cutover_events = state.repo_repo.get_events_since_seq(last_seen, None).await;
|
||||
|
||||
if let Ok(events) = cutover_events
|
||||
&& !events.is_empty()
|
||||
|
||||
@@ -12,12 +12,12 @@ use iroh_car::{CarHeader, CarWriter};
|
||||
use jacquard_repo::commit::Commit;
|
||||
use jacquard_repo::storage::BlockStore;
|
||||
use serde::Serialize;
|
||||
use tranquil_db_traits::RepoRepository;
|
||||
use tranquil_types::Did;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tranquil_db_traits::RepoRepository;
|
||||
use tranquil_types::Did;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -333,7 +333,9 @@ pub async fn format_event_for_sending(
|
||||
frame.prev_data = Some(cid);
|
||||
}
|
||||
let commit_cid = frame.commit;
|
||||
let prev_cid = prev_cid_link.as_ref().and_then(|c| Cid::from_str(c.as_str()).ok());
|
||||
let prev_cid = prev_cid_link
|
||||
.as_ref()
|
||||
.and_then(|c| Cid::from_str(c.as_str()).ok());
|
||||
let mut all_cids: Vec<Cid> = block_cids_str
|
||||
.iter()
|
||||
.filter_map(|s| Cid::from_str(s).ok())
|
||||
@@ -473,7 +475,9 @@ pub async fn format_event_with_prefetched_blocks(
|
||||
frame.prev_data = Some(cid);
|
||||
}
|
||||
let commit_cid = frame.commit;
|
||||
let prev_cid = prev_cid_link.as_ref().and_then(|c| Cid::from_str(c.as_str()).ok());
|
||||
let prev_cid = prev_cid_link
|
||||
.as_ref()
|
||||
.and_then(|c| Cid::from_str(c.as_str()).ok());
|
||||
let mut all_cids: Vec<Cid> = block_cids_str
|
||||
.iter()
|
||||
.filter_map(|s| Cid::from_str(s).ok())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use bytes::Bytes;
|
||||
use cid::Cid;
|
||||
use jacquard::common::IntoStatic;
|
||||
use jacquard::common::types::crypto::PublicKey;
|
||||
use jacquard::common::types::did_doc::DidDocument;
|
||||
use jacquard_common::IntoStatic;
|
||||
use jacquard_common::types::crypto::PublicKey;
|
||||
use jacquard_common::types::did_doc::DidDocument;
|
||||
use jacquard_repo::commit::Commit;
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn test_function() -> &'static str {
|
||||
"NEW_FILE_TEST_67890"
|
||||
}
|
||||
@@ -193,10 +193,11 @@ mod tests {
|
||||
assert_eq!(parts[0].len(), 5);
|
||||
assert_eq!(parts[1].len(), 5);
|
||||
|
||||
assert!(code
|
||||
.chars()
|
||||
.filter(|&c| c != '-')
|
||||
.all(|c| BASE32_ALPHABET.contains(c)));
|
||||
assert!(
|
||||
code.chars()
|
||||
.filter(|&c| c != '-')
|
||||
.all(|c| BASE32_ALPHABET.contains(c))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -91,18 +91,18 @@ impl RecordValidator {
|
||||
validate_datetime(created_at, "createdAt")?;
|
||||
}
|
||||
match record_type {
|
||||
"app.bsky.feed.post" => self.validate_post(obj)?,
|
||||
"app.bsky.actor.profile" => self.validate_profile(obj)?,
|
||||
"app.bsky.feed.like" => self.validate_like(obj)?,
|
||||
"app.bsky.feed.repost" => self.validate_repost(obj)?,
|
||||
"app.bsky.graph.follow" => self.validate_follow(obj)?,
|
||||
"app.bsky.graph.block" => self.validate_block(obj)?,
|
||||
"app.bsky.graph.list" => self.validate_list(obj)?,
|
||||
"app.bsky.graph.listitem" => self.validate_list_item(obj)?,
|
||||
"app.bsky.feed.generator" => self.validate_feed_generator(obj, rkey)?,
|
||||
"app.bsky.feed.threadgate" => self.validate_threadgate(obj)?,
|
||||
"app.bsky.labeler.service" => self.validate_labeler_service(obj)?,
|
||||
"app.bsky.graph.starterpack" => self.validate_starterpack(obj)?,
|
||||
"app.bsky.feed.post" => Self::validate_post(obj)?,
|
||||
"app.bsky.actor.profile" => Self::validate_profile(obj)?,
|
||||
"app.bsky.feed.like" => Self::validate_like(obj)?,
|
||||
"app.bsky.feed.repost" => Self::validate_repost(obj)?,
|
||||
"app.bsky.graph.follow" => Self::validate_follow(obj)?,
|
||||
"app.bsky.graph.block" => Self::validate_block(obj)?,
|
||||
"app.bsky.graph.list" => Self::validate_list(obj)?,
|
||||
"app.bsky.graph.listitem" => Self::validate_list_item(obj)?,
|
||||
"app.bsky.feed.generator" => Self::validate_feed_generator(obj, rkey)?,
|
||||
"app.bsky.feed.threadgate" => Self::validate_threadgate(obj)?,
|
||||
"app.bsky.labeler.service" => Self::validate_labeler_service(obj)?,
|
||||
"app.bsky.graph.starterpack" => Self::validate_starterpack(obj)?,
|
||||
_ => {
|
||||
if self.require_lexicon {
|
||||
return Err(ValidationError::UnknownType(record_type.to_string()));
|
||||
@@ -113,7 +113,7 @@ impl RecordValidator {
|
||||
Ok(ValidationStatus::Valid)
|
||||
}
|
||||
|
||||
fn validate_post(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
fn validate_post(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("text") {
|
||||
return Err(ValidationError::MissingField("text".to_string()));
|
||||
}
|
||||
@@ -186,10 +186,7 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_profile(
|
||||
&self,
|
||||
obj: &serde_json::Map<String, Value>,
|
||||
) -> Result<(), ValidationError> {
|
||||
fn validate_profile(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if let Some(display_name) = obj.get("displayName").and_then(|v| v.as_str()) {
|
||||
let grapheme_count = display_name.chars().count();
|
||||
if grapheme_count > 640 {
|
||||
@@ -227,29 +224,29 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_like(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
fn validate_like(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("subject") {
|
||||
return Err(ValidationError::MissingField("subject".to_string()));
|
||||
}
|
||||
if !obj.contains_key("createdAt") {
|
||||
return Err(ValidationError::MissingField("createdAt".to_string()));
|
||||
}
|
||||
self.validate_strong_ref(obj.get("subject"), "subject")?;
|
||||
Self::validate_strong_ref(obj.get("subject"), "subject")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_repost(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
fn validate_repost(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("subject") {
|
||||
return Err(ValidationError::MissingField("subject".to_string()));
|
||||
}
|
||||
if !obj.contains_key("createdAt") {
|
||||
return Err(ValidationError::MissingField("createdAt".to_string()));
|
||||
}
|
||||
self.validate_strong_ref(obj.get("subject"), "subject")?;
|
||||
Self::validate_strong_ref(obj.get("subject"), "subject")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_follow(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
fn validate_follow(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("subject") {
|
||||
return Err(ValidationError::MissingField("subject".to_string()));
|
||||
}
|
||||
@@ -267,7 +264,7 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_block(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
fn validate_block(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("subject") {
|
||||
return Err(ValidationError::MissingField("subject".to_string()));
|
||||
}
|
||||
@@ -285,7 +282,7 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_list(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
fn validate_list(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("name") {
|
||||
return Err(ValidationError::MissingField("name".to_string()));
|
||||
}
|
||||
@@ -311,10 +308,7 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_list_item(
|
||||
&self,
|
||||
obj: &serde_json::Map<String, Value>,
|
||||
) -> Result<(), ValidationError> {
|
||||
fn validate_list_item(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("subject") {
|
||||
return Err(ValidationError::MissingField("subject".to_string()));
|
||||
}
|
||||
@@ -328,7 +322,6 @@ impl RecordValidator {
|
||||
}
|
||||
|
||||
fn validate_feed_generator(
|
||||
&self,
|
||||
obj: &serde_json::Map<String, Value>,
|
||||
rkey: Option<&str>,
|
||||
) -> Result<(), ValidationError> {
|
||||
@@ -364,10 +357,7 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_starterpack(
|
||||
&self,
|
||||
obj: &serde_json::Map<String, Value>,
|
||||
) -> Result<(), ValidationError> {
|
||||
fn validate_starterpack(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("name") {
|
||||
return Err(ValidationError::MissingField("name".to_string()));
|
||||
}
|
||||
@@ -403,10 +393,7 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_threadgate(
|
||||
&self,
|
||||
obj: &serde_json::Map<String, Value>,
|
||||
) -> Result<(), ValidationError> {
|
||||
fn validate_threadgate(obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("post") {
|
||||
return Err(ValidationError::MissingField("post".to_string()));
|
||||
}
|
||||
@@ -417,7 +404,6 @@ impl RecordValidator {
|
||||
}
|
||||
|
||||
fn validate_labeler_service(
|
||||
&self,
|
||||
obj: &serde_json::Map<String, Value>,
|
||||
) -> Result<(), ValidationError> {
|
||||
if !obj.contains_key("policies") {
|
||||
@@ -429,11 +415,7 @@ impl RecordValidator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_strong_ref(
|
||||
&self,
|
||||
value: Option<&Value>,
|
||||
path: &str,
|
||||
) -> Result<(), ValidationError> {
|
||||
fn validate_strong_ref(value: Option<&Value>, path: &str) -> Result<(), ValidationError> {
|
||||
let obj =
|
||||
value
|
||||
.and_then(|v| v.as_object())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cid::Cid;
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::commit::Commit;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use std::str::FromStr;
|
||||
@@ -14,13 +14,13 @@ fn test_commit_signing_produces_valid_signature() {
|
||||
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
|
||||
let did_typed = jacquard::types::string::Did::new(did).unwrap();
|
||||
let did_typed = jacquard_common::types::string::Did::new(did).unwrap();
|
||||
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None);
|
||||
let signed = unsigned.sign(&signing_key).unwrap();
|
||||
|
||||
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
|
||||
let pubkey = jacquard::types::crypto::PublicKey {
|
||||
codec: jacquard::types::crypto::KeyCodec::Secp256k1,
|
||||
let pubkey = jacquard_common::types::crypto::PublicKey {
|
||||
codec: jacquard_common::types::crypto::KeyCodec::Secp256k1,
|
||||
bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()),
|
||||
};
|
||||
|
||||
@@ -38,13 +38,13 @@ fn test_commit_signing_with_prev() {
|
||||
Cid::from_str("bafyreigxmvutyl3k5m4guzwxv3xf34gfxjlykgfdqkjmf32vwb5vcjxlui").unwrap();
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
|
||||
let did_typed = jacquard::types::string::Did::new(did).unwrap();
|
||||
let did_typed = jacquard_common::types::string::Did::new(did).unwrap();
|
||||
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, Some(prev_cid));
|
||||
let signed = unsigned.sign(&signing_key).unwrap();
|
||||
|
||||
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
|
||||
let pubkey = jacquard::types::crypto::PublicKey {
|
||||
codec: jacquard::types::crypto::KeyCodec::Secp256k1,
|
||||
let pubkey = jacquard_common::types::crypto::PublicKey {
|
||||
codec: jacquard_common::types::crypto::KeyCodec::Secp256k1,
|
||||
bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()),
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ fn test_unsigned_commit_has_5_fields() {
|
||||
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
|
||||
let rev = Tid::from_str("3masrxv55po22").unwrap();
|
||||
|
||||
let did_typed = jacquard::types::string::Did::new(did).unwrap();
|
||||
let did_typed = jacquard_common::types::string::Did::new(did).unwrap();
|
||||
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None);
|
||||
|
||||
let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap();
|
||||
@@ -113,8 +113,8 @@ fn test_create_signed_commit_helper() {
|
||||
let commit = Commit::from_cbor(&signed_bytes).expect("should parse as valid commit");
|
||||
|
||||
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
|
||||
let pubkey = jacquard::types::crypto::PublicKey {
|
||||
codec: jacquard::types::crypto::KeyCodec::Secp256k1,
|
||||
let pubkey = jacquard_common::types::crypto::PublicKey {
|
||||
codec: jacquard_common::types::crypto::KeyCodec::Secp256k1,
|
||||
bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()),
|
||||
};
|
||||
|
||||
|
||||
@@ -149,10 +149,7 @@ async fn test_external_did_web_no_local_doc() {
|
||||
"signingKey": signing_key
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
@@ -431,10 +428,7 @@ async fn test_did_web_byod_flow() {
|
||||
"did": did
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
|
||||
.header("Authorization", format!("Bearer {}", service_jwt))
|
||||
.json(&payload)
|
||||
.send()
|
||||
@@ -494,10 +488,7 @@ async fn test_did_web_byod_flow() {
|
||||
);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.activateAccount",
|
||||
base
|
||||
))
|
||||
.post(format!("{}/xrpc/com.atproto.server.activateAccount", base))
|
||||
.bearer_auth(&access_jwt)
|
||||
.send()
|
||||
.await
|
||||
@@ -525,10 +516,7 @@ async fn test_did_web_byod_flow() {
|
||||
);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.createRecord",
|
||||
base
|
||||
))
|
||||
.post(format!("{}/xrpc/com.atproto.repo.createRecord", base))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"repo": did,
|
||||
|
||||
@@ -51,10 +51,7 @@ async fn test_resolve_handle_not_found() {
|
||||
let _base = base_url().await;
|
||||
let params = [("handle", "nonexistent.handle.test")];
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.identity.resolveHandle",
|
||||
_base
|
||||
))
|
||||
.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", _base))
|
||||
.query(¶ms)
|
||||
.send()
|
||||
.await
|
||||
@@ -149,10 +146,7 @@ async fn test_create_did_web_account_and_resolve() {
|
||||
"signingKey": signing_key
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
@@ -274,10 +268,7 @@ async fn test_did_web_lifecycle() {
|
||||
"signingKey": signing_key
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
|
||||
.json(&create_payload)
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -2,7 +2,7 @@ mod common;
|
||||
use cid::Cid;
|
||||
use common::*;
|
||||
use ipld_core::ipld::Ipld;
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::commit::Commit;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use reqwest::StatusCode;
|
||||
@@ -91,7 +91,7 @@ fn create_did_document(
|
||||
|
||||
fn create_signed_commit(did: &str, data_cid: &Cid, signing_key: &SigningKey) -> (Vec<u8>, Cid) {
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
let did = jacquard::types::string::Did::new(did).expect("valid DID");
|
||||
let did = jacquard_common::types::string::Did::new(did).expect("valid DID");
|
||||
let unsigned = Commit::new_unsigned(did, *data_cid, rev, None);
|
||||
let signed = unsigned.sign(signing_key).expect("signing failed");
|
||||
let signed_bytes = signed.to_cbor().expect("serialization failed");
|
||||
|
||||
@@ -43,7 +43,7 @@ async fn test_verify_live_commit() {
|
||||
.expect("Failed to fetch DID doc");
|
||||
let did_doc_text = resp.text().await.expect("Failed to read body");
|
||||
println!("DID doc: {}", did_doc_text);
|
||||
let did_doc: jacquard::common::types::did_doc::DidDocument<'_> =
|
||||
let did_doc: jacquard_common::types::did_doc::DidDocument<'_> =
|
||||
serde_json::from_str(&did_doc_text).expect("Failed to parse DID doc");
|
||||
let pubkey = did_doc
|
||||
.atproto_public_key()
|
||||
@@ -68,7 +68,7 @@ fn commit_unsigned_bytes(commit: &jacquard_repo::commit::Commit<'_>) -> Vec<u8>
|
||||
did: &'a str,
|
||||
version: i64,
|
||||
data: &'a cid::Cid,
|
||||
rev: &'a jacquard::types::string::Tid,
|
||||
rev: &'a jacquard_common::types::string::Tid,
|
||||
prev: Option<&'a cid::Cid>,
|
||||
#[serde(with = "serde_bytes")]
|
||||
sig: &'a [u8],
|
||||
|
||||
@@ -5,8 +5,6 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-types = { workspace = true }
|
||||
|
||||
bytes = { workspace = true }
|
||||
cid = { workspace = true }
|
||||
jacquard-repo = { workspace = true }
|
||||
|
||||
@@ -33,14 +33,14 @@ pub struct ScopeDefinition {
|
||||
pub display_name: &'static str,
|
||||
}
|
||||
|
||||
pub static SCOPE_DEFINITIONS: LazyLock<HashMap<&'static str, ScopeDefinition>> =
|
||||
LazyLock::new(|| {
|
||||
pub static SCOPE_DEFINITIONS: LazyLock<HashMap<&'static str, ScopeDefinition>> = LazyLock::new(
|
||||
|| {
|
||||
let definitions = vec![
|
||||
ScopeDefinition {
|
||||
scope: "atproto",
|
||||
category: ScopeCategory::Core,
|
||||
required: true,
|
||||
description: "Full access to read, write, and manage this account",
|
||||
description: "Full access to read, write, and manage this account (when no granular permissions are specified)",
|
||||
display_name: "Full Account Access",
|
||||
},
|
||||
ScopeDefinition {
|
||||
@@ -109,7 +109,8 @@ pub static SCOPE_DEFINITIONS: LazyLock<HashMap<&'static str, ScopeDefinition>> =
|
||||
];
|
||||
|
||||
definitions.into_iter().map(|d| (d.scope, d)).collect()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_scope_definition(scope: &str) -> Option<&'static ScopeDefinition> {
|
||||
|
||||
@@ -57,11 +57,9 @@ impl BlobScope {
|
||||
}
|
||||
self.accept.iter().any(|pattern| {
|
||||
pattern == mime
|
||||
|| pattern
|
||||
.strip_suffix("/*")
|
||||
.is_some_and(|prefix| {
|
||||
mime.starts_with(prefix) && mime.chars().nth(prefix.len()) == Some('/')
|
||||
})
|
||||
|| pattern.strip_suffix("/*").is_some_and(|prefix| {
|
||||
mime.starts_with(prefix) && mime.chars().nth(prefix.len()) == Some('/')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -84,6 +82,7 @@ pub enum AccountAttr {
|
||||
Handle,
|
||||
Repo,
|
||||
Status,
|
||||
Wildcard,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -104,6 +103,7 @@ impl AccountAttr {
|
||||
"handle" => Some(Self::Handle),
|
||||
"repo" => Some(Self::Repo),
|
||||
"status" => Some(Self::Status),
|
||||
"*" => Some(Self::Wildcard),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::collections::HashSet;
|
||||
pub struct ScopePermissions {
|
||||
scopes: HashSet<String>,
|
||||
parsed: Vec<ParsedScope>,
|
||||
has_atproto: bool,
|
||||
has_transition_generic: bool,
|
||||
has_transition_chat: bool,
|
||||
has_transition_email: bool,
|
||||
@@ -26,7 +25,7 @@ impl ScopePermissions {
|
||||
let parsed = parse_scope_string(scope_str);
|
||||
|
||||
let has_atproto = parsed.iter().any(|p| matches!(p, ParsedScope::Atproto));
|
||||
let has_transition_generic = parsed
|
||||
let mut has_transition_generic = parsed
|
||||
.iter()
|
||||
.any(|p| matches!(p, ParsedScope::TransitionGeneric));
|
||||
let has_transition_chat = parsed
|
||||
@@ -36,10 +35,24 @@ impl ScopePermissions {
|
||||
.iter()
|
||||
.any(|p| matches!(p, ParsedScope::TransitionEmail));
|
||||
|
||||
let has_granular_scopes = parsed.iter().any(|p| {
|
||||
matches!(
|
||||
p,
|
||||
ParsedScope::Repo(_)
|
||||
| ParsedScope::Blob(_)
|
||||
| ParsedScope::Rpc(_)
|
||||
| ParsedScope::Account(_)
|
||||
| ParsedScope::Identity(_)
|
||||
)
|
||||
});
|
||||
|
||||
if has_atproto && !has_granular_scopes {
|
||||
has_transition_generic = true;
|
||||
}
|
||||
|
||||
Self {
|
||||
scopes,
|
||||
parsed,
|
||||
has_atproto,
|
||||
has_transition_generic,
|
||||
has_transition_chat,
|
||||
has_transition_email,
|
||||
@@ -55,7 +68,7 @@ impl ScopePermissions {
|
||||
}
|
||||
|
||||
pub fn has_full_access(&self) -> bool {
|
||||
self.has_atproto
|
||||
self.has_transition_generic
|
||||
}
|
||||
|
||||
fn find_repo_scopes(&self) -> impl Iterator<Item = &RepoScope> {
|
||||
@@ -109,7 +122,7 @@ impl ScopePermissions {
|
||||
}
|
||||
|
||||
pub fn assert_repo(&self, action: RepoAction, collection: &str) -> Result<(), ScopeError> {
|
||||
if self.has_atproto || self.has_transition_generic {
|
||||
if self.has_transition_generic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -142,11 +155,14 @@ impl ScopePermissions {
|
||||
}
|
||||
|
||||
pub fn assert_blob(&self, mime: &str) -> Result<(), ScopeError> {
|
||||
if self.has_atproto || self.has_transition_generic {
|
||||
if self.has_transition_generic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.find_blob_scopes().any(|blob_scope| blob_scope.matches_mime(mime)) {
|
||||
if self
|
||||
.find_blob_scopes()
|
||||
.any(|blob_scope| blob_scope.matches_mime(mime))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ScopeError::InsufficientScope {
|
||||
@@ -157,7 +173,7 @@ impl ScopePermissions {
|
||||
}
|
||||
|
||||
pub fn assert_rpc(&self, aud: &str, lxm: &str) -> Result<(), ScopeError> {
|
||||
if self.has_atproto || self.has_transition_generic {
|
||||
if self.has_transition_generic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -200,7 +216,7 @@ impl ScopePermissions {
|
||||
attr: AccountAttr,
|
||||
action: AccountAction,
|
||||
) -> Result<(), ScopeError> {
|
||||
if self.has_atproto || self.has_transition_generic {
|
||||
if self.has_transition_generic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -210,9 +226,8 @@ impl ScopePermissions {
|
||||
}
|
||||
|
||||
let has_permission = self.find_account_scopes().any(|account_scope| {
|
||||
account_scope.attr == attr
|
||||
&& (account_scope.action == action
|
||||
|| account_scope.action == AccountAction::Manage)
|
||||
(account_scope.attr == attr || account_scope.attr == AccountAttr::Wildcard)
|
||||
&& (account_scope.action == action || account_scope.action == AccountAction::Manage)
|
||||
});
|
||||
|
||||
if has_permission {
|
||||
@@ -234,12 +249,11 @@ impl ScopePermissions {
|
||||
}
|
||||
|
||||
pub fn allows_email_read(&self) -> bool {
|
||||
self.has_atproto
|
||||
|| self.has_transition_generic
|
||||
self.has_transition_generic
|
||||
|| self.has_transition_email
|
||||
|| self
|
||||
.find_account_scopes()
|
||||
.any(|a| a.attr == AccountAttr::Email)
|
||||
.any(|a| a.attr == AccountAttr::Email || a.attr == AccountAttr::Wildcard)
|
||||
}
|
||||
|
||||
pub fn allows_repo(&self, action: RepoAction, collection: &str) -> bool {
|
||||
@@ -259,15 +273,13 @@ impl ScopePermissions {
|
||||
}
|
||||
|
||||
pub fn assert_identity(&self, attr: IdentityAttr) -> Result<(), ScopeError> {
|
||||
if self.has_atproto || self.has_transition_generic {
|
||||
if self.has_transition_generic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let has_permission = self
|
||||
.find_identity_scopes()
|
||||
.any(|identity_scope| {
|
||||
identity_scope.attr == IdentityAttr::Wildcard || identity_scope.attr == attr
|
||||
});
|
||||
let has_permission = self.find_identity_scopes().any(|identity_scope| {
|
||||
identity_scope.attr == IdentityAttr::Wildcard || identity_scope.attr == attr
|
||||
});
|
||||
|
||||
if has_permission {
|
||||
Ok(())
|
||||
@@ -301,6 +313,7 @@ fn attr_str(attr: AccountAttr) -> &'static str {
|
||||
AccountAttr::Handle => "handle",
|
||||
AccountAttr::Repo => "repo",
|
||||
AccountAttr::Status => "status",
|
||||
AccountAttr::Wildcard => "*",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,4 +498,27 @@ mod tests {
|
||||
assert!(perms.allows_account(AccountAttr::Status, AccountAction::Read));
|
||||
assert!(!perms.allows_account(AccountAttr::Status, AccountAction::Manage));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atproto_with_granular_scopes_uses_granular() {
|
||||
let perms =
|
||||
ScopePermissions::from_scope_string(Some("atproto repo:*?action=create blob:*/*"));
|
||||
assert!(!perms.has_full_access());
|
||||
assert!(perms.allows_repo(RepoAction::Create, "any.collection"));
|
||||
assert!(!perms.allows_repo(RepoAction::Delete, "any.collection"));
|
||||
assert!(!perms.allows_repo(RepoAction::Update, "any.collection"));
|
||||
assert!(perms.allows_blob("image/png"));
|
||||
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atproto_alone_has_full_access() {
|
||||
let perms = ScopePermissions::from_scope_string(Some("atproto"));
|
||||
assert!(perms.has_full_access());
|
||||
assert!(perms.allows_repo(RepoAction::Create, "any.collection"));
|
||||
assert!(perms.allows_repo(RepoAction::Delete, "any.collection"));
|
||||
assert!(perms.allows_repo(RepoAction::Update, "any.collection"));
|
||||
assert!(perms.allows_blob("image/png"));
|
||||
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,3 @@ aws-sdk-s3 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -7,7 +7,7 @@ license.workspace = true
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
cid = { workspace = true }
|
||||
jacquard = { workspace = true }
|
||||
jacquard-common = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
|
||||
+337
-1591
File diff suppressed because it is too large
Load Diff
+29
-11
@@ -9,6 +9,8 @@ const SCOPES = [
|
||||
"repo:*?action=update",
|
||||
"repo:*?action=delete",
|
||||
"blob:*/*",
|
||||
"identity:*",
|
||||
"account:*",
|
||||
].join(" ");
|
||||
|
||||
const CLIENT_ID = !(import.meta.env.DEV)
|
||||
@@ -259,7 +261,7 @@ function extractDPoPNonceFromResponse(response: Response): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function startOAuthLogin(): Promise<void> {
|
||||
export async function startOAuthLogin(loginHint?: string): Promise<void> {
|
||||
clearAllOAuthState();
|
||||
|
||||
const state = generateState();
|
||||
@@ -271,19 +273,24 @@ export async function startOAuthLogin(): Promise<void> {
|
||||
|
||||
saveOAuthState({ state, codeVerifier });
|
||||
|
||||
const parParams: Record<string, string> = {
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
response_type: "code",
|
||||
scope: SCOPES,
|
||||
state: state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
dpop_jkt: dpopJkt,
|
||||
};
|
||||
if (loginHint) {
|
||||
parParams.login_hint = loginHint;
|
||||
}
|
||||
|
||||
const parResponse = await fetch("/oauth/par", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
response_type: "code",
|
||||
scope: SCOPES,
|
||||
state: state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
dpop_jkt: dpopJkt,
|
||||
}),
|
||||
body: new URLSearchParams(parParams),
|
||||
});
|
||||
|
||||
if (!parResponse.ok) {
|
||||
@@ -415,3 +422,14 @@ export function clearOAuthCallbackParams(): void {
|
||||
url.search = "";
|
||||
globalThis.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
|
||||
export async function createDPoPProofForRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
accessToken: string,
|
||||
): Promise<string> {
|
||||
const keyPair = await getOrCreateDPoPKeyPair();
|
||||
const tokenHash = await sha256(accessToken);
|
||||
const ath = base64UrlEncode(tokenHash);
|
||||
return createDPoPProof(keyPair, method, url, getDPoPNonce() ?? undefined, ath);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user