small bug fixes, code dedup

This commit is contained in:
lewis
2026-01-15 19:10:05 +02:00
parent e09567f5cf
commit 505dc169eb
114 changed files with 3473 additions and 3449 deletions
Generated
+2 -478
View File
@@ -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
View File
@@ -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"] }
-1
View File
@@ -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 }
-1
View File
@@ -10,5 +10,4 @@ tranquil-infra = { workspace = true }
async-trait = { workspace = true }
base64 = { workspace = true }
redis = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
-4
View File
@@ -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 }
-1
View File
@@ -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 }
+5 -6
View File
@@ -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>;
+1 -5
View File
@@ -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,
+2 -4
View File
@@ -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>;
+11 -12
View File
@@ -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,
};
+51 -12
View File
@@ -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>;
}
+21 -8
View File
@@ -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>;
+59 -39
View File
@@ -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>;
-1
View File
@@ -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 }
+6 -12
View File
@@ -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 }))
}
}
+57 -66
View File
@@ -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)
}
+1 -1
View File
@@ -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,
+45 -10
View File
@@ -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
+52 -38
View File
@@ -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
+155 -99
View File
@@ -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())
-2
View File
@@ -9,5 +9,3 @@ async-trait = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
+4 -1
View File
@@ -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]
+1 -10
View File
@@ -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(&params.did).await {
let account = match state
.infra_repo
.get_admin_account_info_by_did(&params.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) => {
+36 -29
View File
@@ -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) => {
+13 -8
View File
@@ -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(),
))
+67 -33
View File
@@ -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();
}
+23 -11
View File
@@ -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();
+9 -3
View File
@@ -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()
}
+44 -11
View File
@@ -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,
+58 -44
View File
@@ -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(&current_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(),
};
+35 -32
View File
@@ -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();
}
+3 -4
View File
@@ -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("/");
+7 -7
View File
@@ -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;
+11 -8
View File
@@ -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)
+3 -1
View File
@@ -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);
+12 -13
View File
@@ -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);
+6 -2
View File
@@ -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());
+8 -2
View File
@@ -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);
+15 -4
View File
@@ -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() {
+51 -13
View File
@@ -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();
}
+8 -2
View File
@@ -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");
+37 -24
View File
@@ -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) => {
+40 -29
View File
@@ -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)
+4 -1
View File
@@ -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
+169 -113
View File
@@ -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,
&current_email,
Some(&subject),
&body,
None,
).await
infra_repo
.enqueue_comms(
Some(user_id),
tranquil_db_traits::CommsChannel::Email,
CommsType::EmailUpdate,
&current_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
}
}
+8 -14
View File
@@ -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 {
+4
View File
@@ -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),
+4 -1
View File
@@ -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 -1
View File
@@ -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);
}
}
});
+4 -1
View File
@@ -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(),
+3 -5
View File
@@ -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)
}
+35 -17
View File
@@ -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!(
+13 -15
View File
@@ -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);
+10 -7
View File
@@ -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);
+11 -9
View File
@@ -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();
};
+1 -1
View File
@@ -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()
}
+6 -1
View File
@@ -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);
+4 -1
View File
@@ -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()
+8 -4
View File
@@ -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())
+3 -3
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
pub fn test_function() -> &'static str {
"NEW_FILE_TEST_67890"
}
+5 -4
View File
@@ -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]
+25 -43
View File
@@ -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())
+10 -10
View File
@@ -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()),
};
+4 -16
View File
@@ -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,
+3 -12
View File
@@ -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(&params)
.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],
-2
View File
@@ -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 }
+5 -4
View File
@@ -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> {
+5 -5
View File
@@ -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,
}
}
+57 -21
View File
@@ -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"));
}
}
-2
View File
@@ -13,5 +13,3 @@ aws-sdk-s3 = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
+1 -1
View File
@@ -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 }
File diff suppressed because it is too large Load Diff
+29 -11
View File
@@ -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