From 505dc169eb42bbb81451abb6c7b345fa76d51291 Mon Sep 17 00:00:00 2001 From: lewis Date: Thu, 15 Jan 2026 19:10:05 +0200 Subject: [PATCH] small bug fixes, code dedup --- Cargo.lock | 480 +--- Cargo.toml | 3 +- crates/tranquil-auth/Cargo.toml | 1 - crates/tranquil-cache/Cargo.toml | 1 - crates/tranquil-comms/Cargo.toml | 4 - crates/tranquil-crypto/Cargo.toml | 1 - crates/tranquil-db-traits/src/backup.rs | 11 +- crates/tranquil-db-traits/src/blob.rs | 6 +- crates/tranquil-db-traits/src/delegation.rs | 1 + crates/tranquil-db-traits/src/infra.rs | 6 +- crates/tranquil-db-traits/src/lib.rs | 23 +- crates/tranquil-db-traits/src/oauth.rs | 63 +- crates/tranquil-db-traits/src/repo.rs | 29 +- crates/tranquil-db-traits/src/user.rs | 98 +- crates/tranquil-db/Cargo.toml | 1 - crates/tranquil-db/src/postgres/blob.rs | 18 +- crates/tranquil-db/src/postgres/delegation.rs | 11 +- .../src/postgres/event_notifier.rs | 7 +- crates/tranquil-db/src/postgres/infra.rs | 123 +- crates/tranquil-db/src/postgres/mod.rs | 2 +- crates/tranquil-db/src/postgres/oauth.rs | 55 +- crates/tranquil-db/src/postgres/repo.rs | 90 +- crates/tranquil-db/src/postgres/user.rs | 254 ++- crates/tranquil-infra/Cargo.toml | 2 - crates/tranquil-oauth/src/dpop.rs | 5 +- crates/tranquil-pds/Cargo.toml | 11 +- .../tranquil-pds/src/api/actor/preferences.rs | 9 +- .../src/api/admin/account/info.rs | 47 +- .../src/api/admin/account/update.rs | 40 +- crates/tranquil-pds/src/api/admin/invite.rs | 65 +- crates/tranquil-pds/src/api/admin/status.rs | 21 +- crates/tranquil-pds/src/api/backup.rs | 100 +- crates/tranquil-pds/src/api/delegation.rs | 34 +- crates/tranquil-pds/src/api/error.rs | 12 +- .../tranquil-pds/src/api/identity/account.rs | 55 +- crates/tranquil-pds/src/api/identity/did.rs | 102 +- crates/tranquil-pds/src/api/moderation/mod.rs | 67 +- .../src/api/notification_prefs.rs | 19 +- crates/tranquil-pds/src/api/proxy.rs | 7 +- crates/tranquil-pds/src/api/repo/blob.rs | 14 +- crates/tranquil-pds/src/api/repo/import.rs | 19 +- crates/tranquil-pds/src/api/repo/meta.rs | 4 +- .../tranquil-pds/src/api/repo/record/batch.rs | 12 +- .../src/api/repo/record/delete.rs | 10 +- .../tranquil-pds/src/api/repo/record/read.rs | 25 +- .../tranquil-pds/src/api/repo/record/utils.rs | 93 +- .../tranquil-pds/src/api/repo/record/write.rs | 43 +- .../src/api/server/account_status.rs | 99 +- .../src/api/server/app_password.rs | 13 +- crates/tranquil-pds/src/api/server/email.rs | 8 +- crates/tranquil-pds/src/api/server/invite.rs | 10 +- .../tranquil-pds/src/api/server/migration.rs | 7 +- .../src/api/server/passkey_account.rs | 55 +- .../tranquil-pds/src/api/server/passkeys.rs | 19 +- .../tranquil-pds/src/api/server/password.rs | 64 +- crates/tranquil-pds/src/api/server/reauth.rs | 10 +- .../src/api/server/service_auth.rs | 4 +- crates/tranquil-pds/src/api/server/session.rs | 61 +- .../src/api/server/signing_key.rs | 7 +- crates/tranquil-pds/src/api/server/totp.rs | 69 +- .../src/api/server/trusted_devices.rs | 13 +- .../src/api/server/verify_token.rs | 12 +- crates/tranquil-pds/src/auth/mod.rs | 5 +- crates/tranquil-pds/src/comms/service.rs | 282 ++- crates/tranquil-pds/src/delegation/scopes.rs | 22 +- crates/tranquil-pds/src/lib.rs | 4 + crates/tranquil-pds/src/main.rs | 5 +- crates/tranquil-pds/src/oauth/db/token.rs | 2 +- .../src/oauth/endpoints/authorize.rs | 630 ++++-- .../src/oauth/endpoints/delegation.rs | 198 +- .../tranquil-pds/src/oauth/endpoints/par.rs | 2 +- .../src/oauth/endpoints/token/grants.rs | 26 +- crates/tranquil-pds/src/oauth/verify.rs | 5 +- crates/tranquil-pds/src/plc/mod.rs | 8 +- crates/tranquil-pds/src/scheduled.rs | 52 +- crates/tranquil-pds/src/sync/blob.rs | 28 +- crates/tranquil-pds/src/sync/commit.rs | 17 +- crates/tranquil-pds/src/sync/deprecated.rs | 20 +- crates/tranquil-pds/src/sync/frame.rs | 2 +- crates/tranquil-pds/src/sync/listener.rs | 7 +- crates/tranquil-pds/src/sync/repo.rs | 5 +- .../tranquil-pds/src/sync/subscribe_repos.rs | 12 +- crates/tranquil-pds/src/sync/util.rs | 12 +- crates/tranquil-pds/src/sync/verify.rs | 6 +- crates/tranquil-pds/src/test_new_file.rs | 3 + crates/tranquil-pds/src/util.rs | 9 +- crates/tranquil-pds/src/validation/mod.rs | 68 +- crates/tranquil-pds/tests/commit_signing.rs | 20 +- crates/tranquil-pds/tests/did_web.rs | 20 +- crates/tranquil-pds/tests/identity.rs | 15 +- .../tests/import_with_verification.rs | 4 +- .../tranquil-pds/tests/verify_live_commit.rs | 4 +- crates/tranquil-repo/Cargo.toml | 2 - crates/tranquil-scopes/src/definitions.rs | 9 +- crates/tranquil-scopes/src/parser.rs | 10 +- crates/tranquil-scopes/src/permissions.rs | 78 +- crates/tranquil-storage/Cargo.toml | 2 - crates/tranquil-types/Cargo.toml | 2 +- crates/tranquil-types/src/lib.rs | 1928 +++-------------- frontend/src/lib/oauth.ts | 40 +- frontend/src/lib/registration/flow.svelte.ts | 45 + frontend/src/lib/registration/storage.ts | 195 ++ frontend/src/locales/en.json | 54 +- frontend/src/locales/fi.json | 48 +- frontend/src/locales/ja.json | 48 +- frontend/src/locales/ko.json | 48 +- frontend/src/locales/sv.json | 48 +- frontend/src/locales/zh.json | 48 +- frontend/src/routes/ActAs.svelte | 36 +- frontend/src/routes/Controllers.svelte | 101 +- frontend/src/routes/OAuthConsent.svelte | 96 +- frontend/src/routes/OAuthLogin.svelte | 6 +- frontend/src/routes/Register.svelte | 13 +- frontend/src/routes/RegisterPasskey.svelte | 17 +- 114 files changed, 3473 insertions(+), 3449 deletions(-) create mode 100644 crates/tranquil-pds/src/test_new_file.rs create mode 100644 frontend/src/lib/registration/storage.ts diff --git a/Cargo.lock b/Cargo.lock index 010e337..7d69d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index b0d8cc4..fcb00ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/crates/tranquil-auth/Cargo.toml b/crates/tranquil-auth/Cargo.toml index a9bce57..4831e96 100644 --- a/crates/tranquil-auth/Cargo.toml +++ b/crates/tranquil-auth/Cargo.toml @@ -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 } diff --git a/crates/tranquil-cache/Cargo.toml b/crates/tranquil-cache/Cargo.toml index 2badc06..d6b0012 100644 --- a/crates/tranquil-cache/Cargo.toml +++ b/crates/tranquil-cache/Cargo.toml @@ -10,5 +10,4 @@ tranquil-infra = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } redis = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/tranquil-comms/Cargo.toml b/crates/tranquil-comms/Cargo.toml index aa8cb13..870e1b6 100644 --- a/crates/tranquil-comms/Cargo.toml +++ b/crates/tranquil-comms/Cargo.toml @@ -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 } diff --git a/crates/tranquil-crypto/Cargo.toml b/crates/tranquil-crypto/Cargo.toml index fc6317b..ffd39db 100644 --- a/crates/tranquil-crypto/Cargo.toml +++ b/crates/tranquil-crypto/Cargo.toml @@ -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 } diff --git a/crates/tranquil-db-traits/src/backup.rs b/crates/tranquil-db-traits/src/backup.rs index ee5d4ba..92ec911 100644 --- a/crates/tranquil-db-traits/src/backup.rs +++ b/crates/tranquil-db-traits/src/backup.rs @@ -53,10 +53,7 @@ pub struct BlobExportInfo { #[async_trait] pub trait BackupRepository: Send + Sync { - async fn get_user_backup_status( - &self, - did: &Did, - ) -> Result, DbError>; + async fn get_user_backup_status(&self, did: &Did) -> Result, DbError>; async fn list_backups_for_user(&self, user_id: Uuid) -> Result, DbError>; @@ -92,8 +89,10 @@ pub trait BackupRepository: Send + Sync { did: &Did, ) -> Result, DbError>; - async fn get_user_deactivated_status(&self, did: &Did) - -> Result>>, DbError>; + async fn get_user_deactivated_status( + &self, + did: &Did, + ) -> Result>>, DbError>; async fn update_backup_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError>; diff --git a/crates/tranquil-db-traits/src/blob.rs b/crates/tranquil-db-traits/src/blob.rs index ed1c697..ae87021 100644 --- a/crates/tranquil-db-traits/src/blob.rs +++ b/crates/tranquil-db-traits/src/blob.rs @@ -58,11 +58,7 @@ pub trait BlobRepository: Send + Sync { limit: i64, ) -> Result, DbError>; - async fn list_blobs_since_rev( - &self, - did: &Did, - since: &str, - ) -> Result, DbError>; + async fn list_blobs_since_rev(&self, did: &Did, since: &str) -> Result, DbError>; async fn count_blobs_by_user(&self, user_id: Uuid) -> Result; diff --git a/crates/tranquil-db-traits/src/delegation.rs b/crates/tranquil-db-traits/src/delegation.rs index 2f63344..781bbf7 100644 --- a/crates/tranquil-db-traits/src/delegation.rs +++ b/crates/tranquil-db-traits/src/delegation.rs @@ -112,6 +112,7 @@ pub trait DelegationRepository: Send + Sync { async fn controls_any_accounts(&self, did: &Did) -> Result; + #[allow(clippy::too_many_arguments)] async fn log_delegation_action( &self, delegated_did: &Did, diff --git a/crates/tranquil-db-traits/src/infra.rs b/crates/tranquil-db-traits/src/infra.rs index f529903..04d5baf 100644 --- a/crates/tranquil-db-traits/src/infra.rs +++ b/crates/tranquil-db-traits/src/infra.rs @@ -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, @@ -287,10 +288,7 @@ pub trait InfraRepository: Send + Sync { limit: i64, ) -> Result, DbError>; - async fn get_server_configs( - &self, - keys: &[&str], - ) -> Result, DbError>; + async fn get_server_configs(&self, keys: &[&str]) -> Result, DbError>; async fn upsert_server_config(&self, key: &str, value: &str) -> Result<(), DbError>; diff --git a/crates/tranquil-db-traits/src/lib.rs b/crates/tranquil-db-traits/src/lib.rs index d629f86..1d25f63 100644 --- a/crates/tranquil-db-traits/src/lib.rs +++ b/crates/tranquil-db-traits/src/lib.rs @@ -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, }; diff --git a/crates/tranquil-db-traits/src/oauth.rs b/crates/tranquil-db-traits/src/oauth.rs index dc36a85..b6f5d45 100644 --- a/crates/tranquil-db-traits/src/oauth.rs +++ b/crates/tranquil-db-traits/src/oauth.rs @@ -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, ) -> Result<(), DbError>; - async fn check_refresh_token_used(&self, refresh_token: &RefreshToken) -> Result, DbError>; + async fn check_refresh_token_used( + &self, + refresh_token: &RefreshToken, + ) -> Result, 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, DbError>; @@ -116,7 +122,11 @@ pub trait OAuthRepository: Send + Sync { did: &Did, keep_count: i64, ) -> Result; - async fn revoke_tokens_for_client(&self, did: &Did, client_id: &ClientId) -> Result; + async fn revoke_tokens_for_client( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result; 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, DbError>; - async fn verify_account_on_device(&self, device_id: &DeviceId, did: &Did) -> Result; + async fn get_device_accounts( + &self, + device_id: &DeviceId, + ) -> Result, DbError>; + async fn verify_account_on_device( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result; async fn check_and_record_dpop_jti(&self, jti: &DPoPProofId) -> Result; async fn cleanup_expired_dpop_jtis(&self, max_age_secs: i64) -> Result; @@ -184,7 +208,10 @@ pub trait OAuthRepository: Send + Sync { ) -> Result, DbError>; async fn increment_2fa_attempts(&self, id: Uuid) -> Result; 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; async fn check_user_2fa_enabled(&self, did: &Did) -> Result; @@ -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, DbError>; - async fn device_belongs_to_user(&self, device_id: &DeviceId, did: &Did) -> Result; + async fn device_belongs_to_user( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result; 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, DbError>; async fn delete_session_by_id(&self, session_id: i32, did: &Did) -> Result; async fn delete_sessions_by_did(&self, did: &Did) -> Result; - async fn delete_sessions_by_did_except(&self, did: &Did, except_token_id: &TokenId) -> Result; + async fn delete_sessions_by_did_except( + &self, + did: &Did, + except_token_id: &TokenId, + ) -> Result; } diff --git a/crates/tranquil-db-traits/src/repo.rs b/crates/tranquil-db-traits/src/repo.rs index ead32f1..d3d5fdd 100644 --- a/crates/tranquil-db-traits/src/repo.rs +++ b/crates/tranquil-db-traits/src/repo.rs @@ -233,6 +233,7 @@ pub trait RepoRepository: Send + Sync { rkey: &Rkey, ) -> Result, 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; - async fn get_record_by_cid(&self, cid: &CidLink) -> Result, DbError>; + async fn get_record_by_cid(&self, cid: &CidLink) + -> Result, 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]) - -> Result<(), DbError>; + async fn delete_user_blocks( + &self, + user_id: Uuid, + block_cids: &[Vec], + ) -> 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; - async fn insert_identity_event(&self, did: &Did, handle: Option<&Handle>) -> Result; + async fn insert_identity_event( + &self, + did: &Did, + handle: Option<&Handle>, + ) -> Result; async fn insert_account_event( &self, @@ -302,7 +314,7 @@ pub trait RepoRepository: Send + Sync { ) -> Result; 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, DbError>; - async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) -> Result, DbError>; + async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) + -> Result, DbError>; async fn notify_update(&self, seq: i64) -> Result<(), DbError>; diff --git a/crates/tranquil-db-traits/src/user.rs b/crates/tranquil-db-traits/src/user.rs index 74beb90..dcdd8a2 100644 --- a/crates/tranquil-db-traits/src/user.rs +++ b/crates/tranquil-db-traits/src/user.rs @@ -139,32 +139,34 @@ pub trait UserRepository: Send + Sync { async fn get_user_key_by_id(&self, user_id: Uuid) -> Result, DbError>; - async fn get_id_and_handle_by_did(&self, did: &Did) -> Result, DbError>; + async fn get_id_and_handle_by_did(&self, did: &Did) + -> Result, DbError>; async fn get_did_web_info_by_handle( &self, handle: &Handle, ) -> Result, DbError>; - async fn get_did_web_overrides(&self, user_id: Uuid) -> Result, DbError>; + async fn get_did_web_overrides( + &self, + user_id: Uuid, + ) -> Result, DbError>; async fn get_handle_by_did(&self, did: &Did) -> Result, DbError>; async fn is_account_active_by_did(&self, did: &Did) -> Result, DbError>; - async fn get_user_for_deletion( - &self, - did: &Did, - ) -> Result, DbError>; + async fn get_user_for_deletion(&self, did: &Did) -> Result, DbError>; - async fn check_handle_exists(&self, handle: &Handle, exclude_user_id: Uuid) -> Result; + async fn check_handle_exists( + &self, + handle: &Handle, + exclude_user_id: Uuid, + ) -> Result; async fn update_handle(&self, user_id: Uuid, handle: &Handle) -> Result<(), DbError>; - async fn get_user_with_key_by_did( - &self, - did: &Did, - ) -> Result, DbError>; + async fn get_user_with_key_by_did(&self, did: &Did) -> Result, DbError>; async fn is_account_migrated(&self, did: &Did) -> Result; @@ -174,7 +176,8 @@ pub trait UserRepository: Send + Sync { async fn get_email_info_by_did(&self, did: &Did) -> Result, DbError>; - async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) -> Result; + async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) + -> Result; 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; - async fn get_notification_prefs(&self, did: &Did) -> Result, DbError>; + async fn get_notification_prefs(&self, did: &Did) + -> Result, DbError>; async fn get_id_handle_email_by_did( &self, did: &Did, ) -> Result, 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, 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, DbError>; @@ -316,7 +327,10 @@ pub trait UserRepository: Send + Sync { async fn get_session_info_by_did(&self, did: &Did) -> Result, DbError>; - async fn get_legacy_login_pref(&self, did: &Did) -> Result, DbError>; + async fn get_legacy_login_pref( + &self, + did: &Did, + ) -> Result, DbError>; async fn update_legacy_login(&self, did: &Did, allow: bool) -> Result; @@ -364,7 +378,8 @@ pub trait UserRepository: Send + Sync { did: &Did, ) -> Result, 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, 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; + async fn set_user_takedown( + &self, + did: &Did, + takedown_ref: Option<&str>, + ) -> Result; 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, DbError>; - async fn get_user_for_did_doc_build(&self, did: &Did) -> Result, DbError>; + async fn get_user_for_did_doc_build( + &self, + did: &Did, + ) -> Result, DbError>; async fn upsert_did_web_overrides( &self, @@ -414,13 +436,12 @@ pub trait UserRepository: Send + Sync { also_known_as: Option>, ) -> 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, DbError>; + ) -> Result, DbError>; async fn get_user_for_passkey_recovery( &self, @@ -442,11 +463,7 @@ pub trait UserRepository: Send + Sync { limit: i64, ) -> Result, DbError>; - async fn delete_account_with_firehose( - &self, - user_id: Uuid, - did: &Did, - ) -> Result; + async fn delete_account_with_firehose(&self, user_id: Uuid, did: &Did) -> Result; async fn create_password_account( &self, @@ -468,7 +485,10 @@ pub trait UserRepository: Send + Sync { input: &MigrationReactivationInput, ) -> Result; - async fn check_handle_available_for_new_account(&self, handle: &Handle) -> Result; + async fn check_handle_available_for_new_account( + &self, + handle: &Handle, + ) -> Result; async fn check_and_consume_invite_code(&self, code: &str) -> Result; diff --git a/crates/tranquil-db/Cargo.toml b/crates/tranquil-db/Cargo.toml index b1c5acb..dd34454 100644 --- a/crates/tranquil-db/Cargo.toml +++ b/crates/tranquil-db/Cargo.toml @@ -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 } diff --git a/crates/tranquil-db/src/postgres/blob.rs b/crates/tranquil-db/src/postgres/blob.rs index 7b61339..8d882e3 100644 --- a/crates/tranquil-db/src/postgres/blob.rs +++ b/crates/tranquil-db/src/postgres/blob.rs @@ -80,13 +80,11 @@ impl BlobRepository for PostgresBlobRepository { } async fn get_blob_storage_key(&self, cid: &CidLink) -> Result, 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, DbError> { + async fn list_blobs_since_rev(&self, did: &Did, since: &str) -> Result, DbError> { let results = sqlx::query_scalar!( r#"SELECT DISTINCT unnest(blobs) as "cid!" FROM repo_seq diff --git a/crates/tranquil-db/src/postgres/delegation.rs b/crates/tranquil-db/src/postgres/delegation.rs index c2fcff6..4b849c8 100644 --- a/crates/tranquil-db/src/postgres/delegation.rs +++ b/crates/tranquil-db/src/postgres/delegation.rs @@ -62,15 +62,18 @@ impl PostgresDelegationRepository { #[async_trait] impl DelegationRepository for PostgresDelegationRepository { async fn is_delegated_account(&self, did: &Did) -> Result { - 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( diff --git a/crates/tranquil-db/src/postgres/event_notifier.rs b/crates/tranquil-db/src/postgres/event_notifier.rs index e12aeeb..fb89620 100644 --- a/crates/tranquil-db/src/postgres/event_notifier.rs +++ b/crates/tranquil-db/src/postgres/event_notifier.rs @@ -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 })) } } diff --git a/crates/tranquil-db/src/postgres/infra.rs b/crates/tranquil-db/src/postgres/infra.rs index 185928d..05e0636 100644 --- a/crates/tranquil-db/src/postgres/infra.rs +++ b/crates/tranquil-db/src/postgres/infra.rs @@ -310,77 +310,69 @@ impl InfraRepository for PostgresInfraRepository { sort: InviteCodeSortOrder, ) -> Result, 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, 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, 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, DbError> { let keys_vec: Vec = 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, 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) } diff --git a/crates/tranquil-db/src/postgres/mod.rs b/crates/tranquil-db/src/postgres/mod.rs index 2f6c304..c6d95c8 100644 --- a/crates/tranquil-db/src/postgres/mod.rs +++ b/crates/tranquil-db/src/postgres/mod.rs @@ -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, diff --git a/crates/tranquil-db/src/postgres/oauth.rs b/crates/tranquil-db/src/postgres/oauth.rs index 118401d..7e6d071 100644 --- a/crates/tranquil-db/src/postgres/oauth.rs +++ b/crates/tranquil-db/src/postgres/oauth.rs @@ -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, DbError> { + async fn check_refresh_token_used( + &self, + refresh_token: &RefreshToken, + ) -> Result, 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 { + async fn revoke_tokens_for_client( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result { 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, DbError> { + async fn get_device_accounts( + &self, + device_id: &DeviceId, + ) -> Result, 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 { + async fn verify_account_on_device( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result { 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 { + async fn device_belongs_to_user( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result { 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 diff --git a/crates/tranquil-db/src/postgres/repo.rs b/crates/tranquil-db/src/postgres/repo.rs index cf3c71e..778efad 100644 --- a/crates/tranquil-db/src/postgres/repo.rs +++ b/crates/tranquil-db/src/postgres/repo.rs @@ -147,22 +147,19 @@ impl RepoRepository for PostgresRepoRepository { } async fn count_repos(&self) -> Result { - 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, 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, DbError> { + async fn get_record_by_cid( + &self, + cid: &CidLink, + ) -> Result, 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 { + async fn insert_identity_event( + &self, + did: &Did, + handle: Option<&Handle>, + ) -> Result { 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, DbError> { + async fn get_repo_root_cid_by_user_id( + &self, + user_id: Uuid, + ) -> Result, 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, DbError> { + async fn get_users_without_blocks(&self) -> Result, DbError> { let rows: Vec<(Uuid, String, Option)> = sqlx::query_as( r#" SELECT u.id as user_id, r.repo_root_cid, r.repo_rev diff --git a/crates/tranquil-db/src/postgres/user.rs b/crates/tranquil-db/src/postgres/user.rs index 349796b..e90431c 100644 --- a/crates/tranquil-db/src/postgres/user.rs +++ b/crates/tranquil-db/src/postgres/user.rs @@ -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, 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, 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, DbError> { + async fn get_did_web_overrides( + &self, + user_id: Uuid, + ) -> Result, 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 { + async fn check_handle_exists( + &self, + handle: &Handle, + exclude_user_id: Uuid, + ) -> Result { 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, DbError> { + async fn get_user_with_key_by_did(&self, did: &Did) -> Result, 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 { + async fn check_email_exists( + &self, + email: &str, + exclude_user_id: Uuid, + ) -> Result { 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, DbError> { + async fn get_notification_prefs( + &self, + did: &Did, + ) -> Result, 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, DbError> { + async fn get_session_info_by_did(&self, did: &Did) -> Result, 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 { + async fn set_user_takedown( + &self, + did: &Did, + takedown_ref: Option<&str>, + ) -> Result { 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, DbError> { + async fn get_user_for_did_doc_build( + &self, + did: &Did, + ) -> Result, 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, DbError> { + async fn get_user_for_passkey_setup( + &self, + did: &Did, + ) -> Result, 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 { + async fn delete_account_with_firehose(&self, user_id: Uuid, did: &Did) -> Result { 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 { - 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>)> = + 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>)> = - 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 { + async fn check_handle_available_for_new_account( + &self, + handle: &Handle, + ) -> Result { let exists: Option<(i32,)> = sqlx::query_as("SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL") .bind(handle.as_str()) diff --git a/crates/tranquil-infra/Cargo.toml b/crates/tranquil-infra/Cargo.toml index 0789a32..4c7436e 100644 --- a/crates/tranquil-infra/Cargo.toml +++ b/crates/tranquil-infra/Cargo.toml @@ -9,5 +9,3 @@ async-trait = { workspace = true } bytes = { workspace = true } futures = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } diff --git a/crates/tranquil-oauth/src/dpop.rs b/crates/tranquil-oauth/src/dpop.rs index 07d8ffa..8610d56 100644 --- a/crates/tranquil-oauth/src/dpop.rs +++ b/crates/tranquil-oauth/src/dpop.rs @@ -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] diff --git a/crates/tranquil-pds/Cargo.toml b/crates/tranquil-pds/Cargo.toml index 81f3b46..04c4dac 100644 --- a/crates/tranquil-pds/Cargo.toml +++ b/crates/tranquil-pds/Cargo.toml @@ -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] diff --git a/crates/tranquil-pds/src/api/actor/preferences.rs b/crates/tranquil-pds/src/api/actor/preferences.rs index 16213e1..bd3b65b 100644 --- a/crates/tranquil-pds/src/api/actor/preferences.rs +++ b/crates/tranquil-pds/src/api/actor/preferences.rs @@ -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(); } diff --git a/crates/tranquil-pds/src/api/admin/account/info.rs b/crates/tranquil-pds/src/api/admin/account/info.rs index 16df6cf..7fd77e9 100644 --- a/crates/tranquil-pds/src/api/admin/account/info.rs +++ b/crates/tranquil-pds/src/api/admin/account/info.rs @@ -70,7 +70,11 @@ pub async fn get_account_info( _auth: BearerAuthAdmin, Query(params): Query, ) -> Response { - let account = match state.infra_repo.get_admin_account_info_by_did(¶ms.did).await { + let account = match state + .infra_repo + .get_admin_account_info_by_did(¶ms.did) + .await + { Ok(Some(a)) => a, Ok(None) => return ApiError::AccountNotFound.into_response(), Err(e) => { @@ -114,7 +118,10 @@ async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option Option> { +async fn get_invites_for_user( + state: &AppState, + user_id: uuid::Uuid, +) -> Option> { 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> = 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 = 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 = 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 = all_invite_codes.iter().map(|(_, c)| c.code.clone()).collect(); + let all_codes: Vec = 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 }); diff --git a/crates/tranquil-pds/src/api/admin/account/update.rs b/crates/tranquil-pds/src/api/admin/account/update.rs index 26dbab4..d29d621 100644 --- a/crates/tranquil-pds/src/api/admin/account/update.rs +++ b/crates/tranquil-pds/src/api/admin/account/update.rs @@ -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) => { diff --git a/crates/tranquil-pds/src/api/admin/invite.rs b/crates/tranquil-pds/src/api/admin/invite.rs index ad9193e..27d5075 100644 --- a/crates/tranquil-pds/src/api/admin/invite.rs +++ b/crates/tranquil-pds/src/api/admin/invite.rs @@ -24,16 +24,14 @@ pub async fn disable_invite_codes( _auth: BearerAuthAdmin, Json(input): Json, ) -> 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 = accounts - .iter() - .filter_map(|a| a.parse().ok()) - .collect(); + let accounts_typed: Vec = + 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> = 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> = + 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 = 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) => { diff --git a/crates/tranquil-pds/src/api/admin/status.rs b/crates/tranquil-pds/src/api/admin/status.rs index 88cc747..e7e8d8f 100644 --- a/crates/tranquil-pds/src/api/admin/status.rs +++ b/crates/tranquil-pds/src/api/admin/status.rs @@ -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(), )) diff --git a/crates/tranquil-pds/src/api/backup.rs b/crates/tranquil-pds/src/api/backup.rs index 59f05b2..0e9f1e2 100644 --- a/crates/tranquil-pds/src/api/backup.rs +++ b/crates/tranquil-pds/src/api/backup.rs @@ -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, 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, 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, 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, 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, ) -> 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(); } diff --git a/crates/tranquil-pds/src/api/delegation.rs b/crates/tranquil-pds/src/api/delegation.rs index 67c998d..24f2b50 100644 --- a/crates/tranquil-pds/src/api/delegation.rs +++ b/crates/tranquil-pds/src/api/delegation.rs @@ -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, 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, 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(); diff --git a/crates/tranquil-pds/src/api/error.rs b/crates/tranquil-pds/src/api/error.rs index 9ca3c2d..701c8cc 100644 --- a/crates/tranquil-pds/src/api/error.rs +++ b/crates/tranquil-pds/src/api/error.rs @@ -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 for ApiError { } } - impl From for ApiError { fn from(e: crate::auth::extractor::AuthError) -> Self { match e { @@ -566,8 +570,8 @@ impl From for ApiError { } } -impl From for ApiError { - fn from(e: jacquard::types::string::AtStrError) -> Self { +impl From 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 for ApiError { } } +#[allow(clippy::result_large_err)] pub fn parse_did(s: &str) -> Result { 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, Response> { s.map(parse_did).transpose() } diff --git a/crates/tranquil-pds/src/api/identity/account.rs b/crates/tranquil-pds/src/api/identity/account.rs index d409b31..c57061b 100644 --- a/crates/tranquil-pds/src/api/identity/account.rs +++ b/crates/tranquil-pds/src/api/identity/account.rs @@ -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, Option) = 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, diff --git a/crates/tranquil-pds/src/api/identity/did.rs b/crates/tranquil-pds/src/api/identity/did.rs index 0fe670d..af22777 100644 --- a/crates/tranquil-pds/src/api/identity/did.rs +++ b/crates/tranquil-pds/src/api/identity/did.rs @@ -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, 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 = 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 = + 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, Path(handle): Path) -> Response { let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); - let full_handle = format!("{}.{}", handle, hostname_for_handles); - let full_handle_typed: Handle = match full_handle.parse() { + let current_handle = format!("{}.{}", handle, hostname_for_handles); + let current_handle_typed: Handle = match current_handle.parse() { Ok(h) => h, - Err(_) => return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(), + Err(_) => { + return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); + } }; - let user = match state.user_repo.get_did_web_info_by_handle(&full_handle_typed).await { + let user = match state + .user_repo + .get_did_web_info_by_handle(¤t_handle_typed) + .await + { Ok(Some(u)) => u, Ok(None) => { return ApiError::NotFoundMsg("User not found".into()).into_response(); @@ -306,7 +312,7 @@ pub async fn user_did_doc(State(state): State, Path(handle): Path, Path(handle): Path return ApiError::InternalError(None).into_response(), Err(_) => return ApiError::InternalError(None).into_response(), }; - let key_bytes: Vec = 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 = + 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, Path(handle): Path 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(), }; diff --git a/crates/tranquil-pds/src/api/moderation/mod.rs b/crates/tranquil-pds/src/api/moderation/mod.rs index 3aac97f..87f56a0 100644 --- a/crates/tranquil-pds/src/api/moderation/mod.rs +++ b/crates/tranquil-pds/src/api/moderation/mod.rs @@ -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(); } diff --git a/crates/tranquil-pds/src/api/notification_prefs.rs b/crates/tranquil-pds/src/api/notification_prefs.rs index dfe1484..d4902d8 100644 --- a/crates/tranquil-pds/src/api/notification_prefs.rs +++ b/crates/tranquil-pds/src/api/notification_prefs.rs @@ -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(); } diff --git a/crates/tranquil-pds/src/api/proxy.rs b/crates/tranquil-pds/src/api/proxy.rs index 40d0797..b3b3905 100644 --- a/crates/tranquil-pds/src/api/proxy.rs +++ b/crates/tranquil-pds/src/api/proxy.rs @@ -150,10 +150,9 @@ impl> Service 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("/"); diff --git a/crates/tranquil-pds/src/api/repo/blob.rs b/crates/tranquil-pds/src/api/repo/blob.rs index 0b4c1ee..53ce825 100644 --- a/crates/tranquil-pds/src/api/repo/blob.rs +++ b/crates/tranquil-pds/src/api/repo/blob.rs @@ -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; diff --git a/crates/tranquil-pds/src/api/repo/import.rs b/crates/tranquil-pds/src/api/repo/import.rs index 2b059f8..ee3af17 100644 --- a/crates/tranquil-pds/src/api/repo/import.rs +++ b/crates/tranquil-pds/src/api/repo/import.rs @@ -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) diff --git a/crates/tranquil-pds/src/api/repo/meta.rs b/crates/tranquil-pds/src/api/repo/meta.rs index ec5cfbc..b2d8bb8 100644 --- a/crates/tranquil-pds/src/api/repo/meta.rs +++ b/crates/tranquil-pds/src/api/repo/meta.rs @@ -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 diff --git a/crates/tranquil-pds/src/api/repo/record/batch.rs b/crates/tranquil-pds/src/api/repo/record/batch.rs index 206f9d9..af4d300 100644 --- a/crates/tranquil-pds/src/api/repo/record/batch.rs +++ b/crates/tranquil-pds/src/api/repo/record/batch.rs @@ -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(), }; diff --git a/crates/tranquil-pds/src/api/repo/record/delete.rs b/crates/tranquil-pds/src/api/repo/record/delete.rs index e8af11c..b417738 100644 --- a/crates/tranquil-pds/src/api/repo/record/delete.rs +++ b/crates/tranquil-pds/src/api/repo/record/delete.rs @@ -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); diff --git a/crates/tranquil-pds/src/api/repo/record/read.rs b/crates/tranquil-pds/src/api/repo/record/read.rs index 99a4e99..0c37b87 100644 --- a/crates/tranquil-pds/src/api/repo/record/read.rs +++ b/crates/tranquil-pds/src/api/repo/record/read.rs @@ -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::().ok()); + let cursor_rkey = input + .cursor + .as_ref() + .and_then(|c| c.parse::().ok()); let rows = match state .repo_repo .list_records( diff --git a/crates/tranquil-pds/src/api/repo/record/utils.rs b/crates/tranquil-pds/src/api/repo/record/utils.rs index 286a9cf..f6e8d07 100644 --- a/crates/tranquil-pds/src/api/repo/record/utils.rs +++ b/crates/tranquil-pds/src/api/repo/record/utils.rs @@ -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) { } } -use tranquil_db_traits::Backlink; use crate::types::AtUri; +use tranquil_db_traits::Backlink; pub fn extract_backlinks(uri: &AtUri, record: &Value) -> Vec { let record_type = record @@ -50,22 +50,26 @@ pub fn extract_backlinks(uri: &AtUri, record: &Value) -> Vec { .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, signing_key: &SigningKey, ) -> Result<(Vec, 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 { - 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 = 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 { - 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) diff --git a/crates/tranquil-pds/src/api/repo/record/write.rs b/crates/tranquil-pds/src/api/repo/record/write.rs index ae3c6c8..c8743db 100644 --- a/crates/tranquil-pds/src/api/repo/record/write.rs +++ b/crates/tranquil-pds/src/api/repo/record/write.rs @@ -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); } ( diff --git a/crates/tranquil-pds/src/api/server/account_status.rs b/crates/tranquil-pds/src/api/server/account_status.rs index 3ce1c78..b4937b1 100644 --- a/crates/tranquil-pds/src/api/server/account_status.rs +++ b/crates/tranquil-pds/src/api/server/account_status.rs @@ -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, did: &crate::types::Did) -> bool { +async fn is_valid_did_for_service( + user_repo: &dyn tranquil_db_traits::UserRepository, + cache: Arc, + 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 diff --git a/crates/tranquil-pds/src/api/server/app_password.rs b/crates/tranquil-pds/src/api/server/app_password.rs index c949233..6a17a17 100644 --- a/crates/tranquil-pds/src/api/server/app_password.rs +++ b/crates/tranquil-pds/src/api/server/app_password.rs @@ -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); diff --git a/crates/tranquil-pds/src/api/server/email.rs b/crates/tranquil-pds/src/api/server/email.rs index afada3b..cb7363e 100644 --- a/crates/tranquil-pds/src/api/server/email.rs +++ b/crates/tranquil-pds/src/api/server/email.rs @@ -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()); diff --git a/crates/tranquil-pds/src/api/server/invite.rs b/crates/tranquil-pds/src/api/server/invite.rs index 5c252c0..d4c50cb 100644 --- a/crates/tranquil-pds/src/api/server/invite.rs +++ b/crates/tranquil-pds/src/api/server/invite.rs @@ -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, }) diff --git a/crates/tranquil-pds/src/api/server/migration.rs b/crates/tranquil-pds/src/api/server/migration.rs index c4ebf49..7e491e7 100644 --- a/crates/tranquil-pds/src/api/server/migration.rs +++ b/crates/tranquil-pds/src/api/server/migration.rs @@ -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) { diff --git a/crates/tranquil-pds/src/api/server/passkey_account.rs b/crates/tranquil-pds/src/api/server/passkey_account.rs index 6d321d2..b84102e 100644 --- a/crates/tranquil-pds/src/api/server/passkey_account.rs +++ b/crates/tranquil-pds/src/api/server/passkey_account.rs @@ -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, Option) = 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); diff --git a/crates/tranquil-pds/src/api/server/passkeys.rs b/crates/tranquil-pds/src/api/server/passkeys.rs index 78b6b14..32ef39d 100644 --- a/crates/tranquil-pds/src/api/server/passkeys.rs +++ b/crates/tranquil-pds/src/api/server/passkeys.rs @@ -269,13 +269,24 @@ pub async fn delete_passkey( auth: BearerAuth, Json(input): Json, ) -> 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() { diff --git a/crates/tranquil-pds/src/api/server/password.rs b/crates/tranquil-pds/src/api/server/password.rs index 5b7dcea..6c9d50d 100644 --- a/crates/tranquil-pds/src/api/server/password.rs +++ b/crates/tranquil-pds/src/api/server/password.rs @@ -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, ) -> 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, auth: BearerAuth } pub async fn remove_password(State(state): State, 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, 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(); } diff --git a/crates/tranquil-pds/src/api/server/reauth.rs b/crates/tranquil-pds/src/api/server/reauth.rs index d429ad6..cfd3198 100644 --- a/crates/tranquil-pds/src/api/server/reauth.rs +++ b/crates/tranquil-pds/src/api/server/reauth.rs @@ -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 { diff --git a/crates/tranquil-pds/src/api/server/service_auth.rs b/crates/tranquil-pds/src/api/server/service_auth.rs index 55a0fb7..4b8b13a 100644 --- a/crates/tranquil-pds/src/api/server/service_auth.rs +++ b/crates/tranquil-pds/src/api/server/service_auth.rs @@ -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"); diff --git a/crates/tranquil-pds/src/api/server/session.rs b/crates/tranquil-pds/src/api/server/session.rs index 4829aa6..dd3707e 100644 --- a/crates/tranquil-pds/src/api/server/session.rs +++ b/crates/tranquil-pds/src/api/server/session.rs @@ -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, ) -> 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, ) -> 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 diff --git a/crates/tranquil-pds/src/api/server/signing_key.rs b/crates/tranquil-pds/src/api/server/signing_key.rs index cf47a4f..36407b9 100644 --- a/crates/tranquil-pds/src/api/server/signing_key.rs +++ b/crates/tranquil-pds/src/api/server/signing_key.rs @@ -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) => { diff --git a/crates/tranquil-pds/src/api/server/totp.rs b/crates/tranquil-pds/src/api/server/totp.rs index 7c254fb..1dddc3a 100644 --- a/crates/tranquil-pds/src/api/server/totp.rs +++ b/crates/tranquil-pds/src/api/server/totp.rs @@ -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, ) -> 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) } diff --git a/crates/tranquil-pds/src/api/server/trusted_devices.rs b/crates/tranquil-pds/src/api/server/trusted_devices.rs index 5dc2c59..148624d 100644 --- a/crates/tranquil-pds/src/api/server/trusted_devices.rs +++ b/crates/tranquil-pds/src/api/server/trusted_devices.rs @@ -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 } diff --git a/crates/tranquil-pds/src/api/server/verify_token.rs b/crates/tranquil-pds/src/api/server/verify_token.rs index 0154b6c..2dbc3c9 100644 --- a/crates/tranquil-pds/src/api/server/verify_token.rs +++ b/crates/tranquil-pds/src/api/server/verify_token.rs @@ -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, 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, 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) diff --git a/crates/tranquil-pds/src/auth/mod.rs b/crates/tranquil-pds/src/auth/mod.rs index 7366c39..c061105 100644 --- a/crates/tranquil-pds/src/auth/mod.rs +++ b/crates/tranquil-pds/src/auth/mod.rs @@ -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 diff --git a/crates/tranquil-pds/src/comms/service.rs b/crates/tranquil-pds/src/comms/service.rs index 49d54e6..b2a4a13 100644 --- a/crates/tranquil-pds/src/comms/service.rs +++ b/crates/tranquil-pds/src/comms/service.rs @@ -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, 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 { - 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 { - 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 { - let prefs = user_repo.get_comms_prefs(user_id).await?.ok_or(DbError::NotFound)?; + let prefs = user_repo + .get_comms_prefs(user_id) + .await? + .ok_or(DbError::NotFound)?; let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en")); let current_email = prefs.email.unwrap_or_default(); let verify_page = format!("https://{}/app/verify?type=email-update", hostname); @@ -369,15 +391,17 @@ pub mod repo { ], ); let subject = format_message(strings.email_update_subject, &[("hostname", hostname)]); - infra_repo.enqueue_comms( - Some(user_id), - tranquil_db_traits::CommsChannel::Email, - CommsType::EmailUpdate, - ¤t_email, - Some(&subject), - &body, - None, - ).await + infra_repo + .enqueue_comms( + Some(user_id), + tranquil_db_traits::CommsChannel::Email, + CommsType::EmailUpdate, + ¤t_email, + Some(&subject), + &body, + None, + ) + .await } pub async fn enqueue_account_deletion( @@ -387,7 +411,10 @@ pub mod repo { code: &str, hostname: &str, ) -> Result { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 } } diff --git a/crates/tranquil-pds/src/delegation/scopes.rs b/crates/tranquil-pds/src/delegation/scopes.rs index c7905a8..d8e3f6e 100644 --- a/crates/tranquil-pds/src/delegation/scopes.rs +++ b/crates/tranquil-pds/src/delegation/scopes.rs @@ -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::>().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 { diff --git a/crates/tranquil-pds/src/lib.rs b/crates/tranquil-pds/src/lib.rs index 863c5ce..d46364f 100644 --- a/crates/tranquil-pds/src/lib.rs +++ b/crates/tranquil-pds/src/lib.rs @@ -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), diff --git a/crates/tranquil-pds/src/main.rs b/crates/tranquil-pds/src/main.rs index da0a9f4..c44bfdb 100644 --- a/crates/tranquil-pds/src/main.rs +++ b/crates/tranquil-pds/src/main.rs @@ -36,7 +36,10 @@ async fn run() -> Result<(), Box> { 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), diff --git a/crates/tranquil-pds/src/oauth/db/token.rs b/crates/tranquil-pds/src/oauth/db/token.rs index 941068b..cd520a6 100644 --- a/crates/tranquil-pds/src/oauth/db/token.rs +++ b/crates/tranquil-pds/src/oauth/db/token.rs @@ -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, diff --git a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs b/crates/tranquil-pds/src/oauth/endpoints/authorize.rs index f4b5863..c56af78 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/authorize.rs @@ -4,9 +4,7 @@ use crate::oauth::{ db::should_show_consent, }; use crate::state::{AppState, RateLimitKind}; -use tranquil_db_traits::ScopePreference; use crate::types::{Did, Handle, PlainPassword}; -use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId}; use axum::{ Json, extract::{Query, State}, @@ -19,6 +17,8 @@ use axum::{ use chrono::Utc; use serde::{Deserialize, Serialize}; use subtle::ConstantTimeEq; +use tranquil_db_traits::ScopePreference; +use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId}; use urlencoding::encode as url_encode; const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; @@ -207,7 +207,11 @@ pub async fn authorize_get( } }; let request_id = RequestId::from(request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&request_id) + .await + { Ok(Some(data)) => data, Ok(None) => { if wants_json(&headers) { @@ -239,7 +243,10 @@ pub async fn authorize_get( } }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&request_id) + .await; if wants_json(&headers) { return ( StatusCode::BAD_REQUEST, @@ -287,7 +294,11 @@ pub async fn authorize_get( }; tracing::info!(normalized = %normalized, "Normalized login_hint"); - match state.user_repo.get_login_check_by_handle_or_email(&normalized).await { + match state + .user_repo + .get_login_check_by_handle_or_email(&normalized) + .await + { Ok(Some(user)) => { tracing::info!(did = %user.did, has_password = user.password_hash.is_some(), "Found user for login_hint"); let is_delegated = state @@ -298,7 +309,7 @@ pub async fn authorize_get( let has_password = user.password_hash.is_some(); tracing::info!(is_delegated = %is_delegated, has_password = %has_password, "Delegation check"); - if is_delegated && !has_password { + if is_delegated { tracing::info!("Redirecting to delegation auth"); return redirect_see_other(&format!( "/app/oauth/delegation?request_uri={}&delegated_did={}", @@ -320,7 +331,10 @@ pub async fn authorize_get( if !force_new_account && let Some(device_id) = extract_device_cookie(&headers) - && let Ok(accounts) = state.oauth_repo.get_device_accounts(&DeviceIdType::from(device_id.clone())).await + && let Ok(accounts) = state + .oauth_repo + .get_device_accounts(&DeviceIdType::from(device_id.clone())) + .await && !accounts.is_empty() { return redirect_see_other(&format!( @@ -342,12 +356,17 @@ pub async fn authorize_get_json( .request_uri .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?; let request_id_json = RequestId::from(request_uri.clone()); - let request_data = state.oauth_repo.get_authorization_request(&request_id_json) + let request_data = state + .oauth_repo + .get_authorization_request(&request_id_json) .await .map_err(crate::oauth::db_err_to_oauth)? .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&request_id_json).await; + let _ = state + .oauth_repo + .delete_authorization_request(&request_id_json) + .await; return Err(OAuthError::InvalidRequest( "request_uri has expired".to_string(), )); @@ -474,7 +493,11 @@ pub async fn authorize_post( ); } let form_request_id = RequestId::from(form.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&form_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&form_request_id) + .await + { Ok(Some(data)) => data, Ok(None) => { if json_response { @@ -507,7 +530,10 @@ pub async fn authorize_post( } }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&form_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&form_request_id) + .await; if json_response { return ( axum::http::StatusCode::BAD_REQUEST, @@ -559,7 +585,11 @@ pub async fn authorize_post( pds_hostname = %pds_hostname, "Normalized username for lookup" ); - let user = match state.user_repo.get_login_info_by_handle_or_email(&normalized_username).await { + let user = match state + .user_repo + .get_login_info_by_handle_or_email(&normalized_username) + .await + { Ok(Some(u)) => u, Ok(None) => { let _ = bcrypt::verify( @@ -588,7 +618,9 @@ pub async fn authorize_post( } if user.account_type == "delegated" { - if state.oauth_repo.set_authorization_did(&form_request_id, &user.did, None) + if state + .oauth_repo + .set_authorization_did(&form_request_id, &user.did, None) .await .is_err() { @@ -614,7 +646,9 @@ pub async fn authorize_post( } if !user.password_required { - if state.oauth_repo.set_authorization_did(&form_request_id, &user.did, None) + if state + .oauth_repo + .set_authorization_did(&form_request_id, &user.did, None) .await .is_err() { @@ -653,17 +687,21 @@ pub async fn authorize_post( if has_totp { let device_cookie = extract_device_cookie(&headers); let device_is_trusted = if let Some(ref dev_id) = device_cookie { - crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did).await + crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did) + .await } else { false }; if device_is_trusted { if let Some(ref dev_id) = device_cookie { - let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id).await; + let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) + .await; } } else { - if state.oauth_repo.set_authorization_did(&form_request_id, &user.did, None) + if state + .oauth_repo + .set_authorization_did(&form_request_id, &user.did, None) .await .is_err() { @@ -682,13 +720,26 @@ pub async fn authorize_post( } } if user.two_factor_enabled { - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&form_request_id).await; - match state.oauth_repo.create_2fa_challenge(&user.did, &form_request_id).await { + let _ = state + .oauth_repo + .delete_2fa_challenge_by_request_uri(&form_request_id) + .await; + match state + .oauth_repo + .create_2fa_challenge(&user.did, &form_request_id) + .await + { Ok(challenge) => { let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); - if let Err(e) = - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &hostname).await + if let Err(e) = enqueue_2fa_code( + state.user_repo.as_ref(), + state.infra_repo.as_ref(), + user.id, + &challenge.code, + &hostname, + ) + .await { tracing::warn!( did = %user.did, @@ -729,7 +780,9 @@ pub async fn authorize_post( last_seen_at: Utc::now(), }; let new_device_id_typed = DeviceIdType::from(new_id.0.clone()); - if state.oauth_repo.create_device(&new_device_id_typed, &device_data) + if state + .oauth_repo + .create_device(&new_device_id_typed, &device_data) .await .is_ok() { @@ -739,7 +792,10 @@ pub async fn authorize_post( new_id.0 }; let final_device_typed = DeviceIdType::from(final_device_id.clone()); - let _ = state.oauth_repo.upsert_account_device(&user.did, &final_device_typed).await; + let _ = state + .oauth_repo + .upsert_account_device(&user.did, &final_device_typed) + .await; } let set_auth_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); if state @@ -796,14 +852,16 @@ pub async fn authorize_post( let code = Code::generate(); let auth_post_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); let auth_post_code = AuthorizationCode::from(code.0.clone()); - if state.oauth_repo.update_authorization_request( - &form_request_id, - &user.did, - auth_post_device_id.as_ref(), - &auth_post_code, - ) - .await - .is_err() + if state + .oauth_repo + .update_authorization_request( + &form_request_id, + &user.did, + auth_post_device_id.as_ref(), + &auth_post_code, + ) + .await + .is_err() { return show_login_error("An error occurred. Please try again.", json_response); } @@ -859,7 +917,11 @@ pub async fn authorize_select( .into_response() }; let select_request_id = RequestId::from(form.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&select_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&select_request_id) + .await + { Ok(Some(data)) => data, Ok(None) => { return json_error( @@ -877,7 +939,10 @@ pub async fn authorize_select( } }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&select_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&select_request_id) + .await; return json_error( StatusCode::BAD_REQUEST, "invalid_request", @@ -905,7 +970,11 @@ pub async fn authorize_select( } }; let verify_device_id = DeviceIdType::from(device_id.clone()); - let account_valid = match state.oauth_repo.verify_account_on_device(&verify_device_id, &did).await { + let account_valid = match state + .oauth_repo + .verify_account_on_device(&verify_device_id, &did) + .await + { Ok(valid) => valid, Err(_) => { return json_error( @@ -953,7 +1022,9 @@ pub async fn authorize_select( let has_totp = crate::api::server::has_totp_enabled(&state, &did).await; let select_early_device_typed = DeviceIdType::from(device_id.clone()); if has_totp { - if state.oauth_repo.set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed)) + if state + .oauth_repo + .set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed)) .await .is_err() { @@ -969,13 +1040,26 @@ pub async fn authorize_select( .into_response(); } if user.two_factor_enabled { - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&select_request_id).await; - match state.oauth_repo.create_2fa_challenge(&did, &select_request_id).await { + let _ = state + .oauth_repo + .delete_2fa_challenge_by_request_uri(&select_request_id) + .await; + match state + .oauth_repo + .create_2fa_challenge(&did, &select_request_id) + .await + { Ok(challenge) => { let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); - if let Err(e) = - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &hostname).await + if let Err(e) = enqueue_2fa_code( + state.user_repo.as_ref(), + state.infra_repo.as_ref(), + user.id, + &challenge.code, + &hostname, + ) + .await { tracing::warn!( did = %form.did, @@ -1000,17 +1084,22 @@ pub async fn authorize_select( } } let select_device_typed = DeviceIdType::from(device_id.clone()); - let _ = state.oauth_repo.upsert_account_device(&did, &select_device_typed).await; + let _ = state + .oauth_repo + .upsert_account_device(&did, &select_device_typed) + .await; let code = Code::generate(); let select_code = AuthorizationCode::from(code.0.clone()); - if state.oauth_repo.update_authorization_request( - &select_request_id, - &did, - Some(&select_device_typed), - &select_code, - ) - .await - .is_err() + if state + .oauth_repo + .update_authorization_request( + &select_request_id, + &did, + Some(&select_device_typed), + &select_code, + ) + .await + .is_err() { return json_error( StatusCode::INTERNAL_SERVER_ERROR, @@ -1121,7 +1210,11 @@ pub async fn authorize_deny( Json(form): Json, ) -> Response { let deny_request_id = RequestId::from(form.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&deny_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&deny_request_id) + .await + { Ok(Some(data)) => data, Ok(None) => { return ( @@ -1144,7 +1237,10 @@ pub async fn authorize_deny( .into_response(); } }; - let _ = state.oauth_repo.delete_authorization_request(&deny_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&deny_request_id) + .await; let redirect_uri = &request_data.parameters.redirect_uri; let mut redirect_url = redirect_uri.to_string(); let separator = if redirect_url.contains('?') { '&' } else { '?' }; @@ -1208,7 +1304,11 @@ pub async fn authorize_2fa_get( "2FA code has expired. Please start over.", ); } - let _request_data = match state.oauth_repo.get_authorization_request(&twofa_request_id).await { + let _request_data = match state + .oauth_repo + .get_authorization_request(&twofa_request_id) + .await + { Ok(Some(d)) => d, Ok(None) => { return redirect_to_frontend_error( @@ -1252,6 +1352,8 @@ pub struct ConsentResponse { pub show_consent: bool, pub did: String, #[serde(skip_serializing_if = "Option::is_none")] + pub handle: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub is_delegation: Option, #[serde(skip_serializing_if = "Option::is_none")] pub controller_did: Option, @@ -1278,29 +1380,35 @@ pub async fn consent_get( Query(query): Query, ) -> Response { let consent_request_id = RequestId::from(query.request_uri.clone()); - let request_data = - match state.oauth_repo.get_authorization_request(&consent_request_id).await { - Ok(Some(data)) => data, - Ok(None) => { - return json_error( - StatusCode::BAD_REQUEST, - "invalid_request", - "Invalid or expired request_uri", - ); - } - Err(e) => { - return json_error( - StatusCode::INTERNAL_SERVER_ERROR, - "server_error", - &format!("Database error: {:?}", e), - ); - } - }; + let request_data = match state + .oauth_repo + .get_authorization_request(&consent_request_id) + .await + { + Ok(Some(data)) => data, + Ok(None) => { + return json_error( + StatusCode::BAD_REQUEST, + "invalid_request", + "Invalid or expired request_uri", + ); + } + Err(e) => { + return json_error( + StatusCode::INTERNAL_SERVER_ERROR, + "server_error", + &format!("Database error: {:?}", e), + ); + } + }; let flow_state = AuthFlowState::from_request_data(&request_data); if let Some(err_response) = validate_auth_flow_state(&flow_state, true) { if flow_state.is_expired() { - let _ = state.oauth_repo.delete_authorization_request(&consent_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&consent_request_id) + .await; } return err_response; } @@ -1328,7 +1436,10 @@ pub async fn consent_get( .filter(|s| !s.trim().is_empty()) .unwrap_or("atproto"); - let controller_did_parsed: Option = request_data.controller_did.as_ref().and_then(|s| s.parse().ok()); + let controller_did_parsed: Option = request_data + .controller_did + .as_ref() + .and_then(|s| s.parse().ok()); let delegation_grant = if let Some(ref ctrl_did) = controller_did_parsed { state .delegation_repo @@ -1348,10 +1459,11 @@ pub async fn consent_get( let requested_scopes: Vec<&str> = effective_scope_str.split_whitespace().collect(); let consent_client_id = ClientId::from(request_data.parameters.client_id.clone()); - let preferences = - state.oauth_repo.get_scope_preferences(&did, &consent_client_id) - .await - .unwrap_or_default(); + let preferences = state + .oauth_repo + .get_scope_preferences(&did, &consent_client_id) + .await + .unwrap_or_default(); let pref_map: std::collections::HashMap<_, _> = preferences .iter() .map(|p| (p.scope.as_str(), p.granted)) @@ -1366,32 +1478,45 @@ pub async fn consent_get( ) .await .unwrap_or(true); + let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s)); let scopes: Vec = requested_scopes .iter() .map(|scope| { - let (category, required, description, display_name) = - if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) { - ( - def.category.display_name().to_string(), - def.required, - def.description.to_string(), - def.display_name.to_string(), - ) - } else if scope.starts_with("ref:") { - ( - "Reference".to_string(), - false, - "Referenced scope".to_string(), - scope.to_string(), - ) + let (category, required, description, display_name) = if let Some(def) = + crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) + { + let desc = if *scope == "atproto" && has_granular_scopes { + "AT Protocol baseline scope (permissions determined by selected options below)" + .to_string() } else { - ( - "Other".to_string(), - false, - format!("Access to {}", scope), - scope.to_string(), - ) + def.description.to_string() }; + let name = if *scope == "atproto" && has_granular_scopes { + "AT Protocol Access".to_string() + } else { + def.display_name.to_string() + }; + ( + def.category.display_name().to_string(), + def.required, + desc, + name, + ) + } else if scope.starts_with("ref:") { + ( + "Reference".to_string(), + false, + "Referenced scope".to_string(), + scope.to_string(), + ) + } else { + ( + "Other".to_string(), + false, + format!("Access to {}", scope), + scope.to_string(), + ) + }; let granted = pref_map.get(*scope).copied(); ScopeInfo { scope: scope.to_string(), @@ -1403,6 +1528,15 @@ pub async fn consent_get( } }) .collect(); + + let account_handle = state + .user_repo + .get_handle_by_did(&did) + .await + .ok() + .flatten() + .map(|h| h.to_string()); + let (is_delegation, controller_did_resp, controller_handle, delegation_level) = if let Some(ref ctrl_did) = controller_did_parsed { let ctrl_handle = state @@ -1414,7 +1548,9 @@ pub async fn consent_get( .map(|h| h.to_string()); let level = if let Some(ref grant) = delegation_grant { - let preset = crate::delegation::SCOPE_PRESETS.iter().find(|p| p.scopes == grant.granted_scopes); + let preset = crate::delegation::SCOPE_PRESETS + .iter() + .find(|p| p.scopes == grant.granted_scopes); preset .map(|p| p.label.to_string()) .unwrap_or_else(|| "Custom".to_string()) @@ -1422,7 +1558,12 @@ pub async fn consent_get( "Unknown".to_string() }; - (Some(true), Some(ctrl_did.to_string()), ctrl_handle, Some(level)) + ( + Some(true), + Some(ctrl_did.to_string()), + ctrl_handle, + Some(level), + ) } else { (None, None, None, None) }; @@ -1436,6 +1577,7 @@ pub async fn consent_get( scopes, show_consent, did: did_str, + handle: account_handle, is_delegation, controller_did: controller_did_resp, controller_handle, @@ -1454,28 +1596,34 @@ pub async fn consent_post( form.remember ); let consent_post_request_id = RequestId::from(form.request_uri.clone()); - let request_data = - match state.oauth_repo.get_authorization_request(&consent_post_request_id).await { - Ok(Some(data)) => data, - Ok(None) => { - return json_error( - StatusCode::BAD_REQUEST, - "invalid_request", - "Invalid or expired request_uri", - ); - } - Err(e) => { - return json_error( - StatusCode::INTERNAL_SERVER_ERROR, - "server_error", - &format!("Database error: {:?}", e), - ); - } - }; + let request_data = match state + .oauth_repo + .get_authorization_request(&consent_post_request_id) + .await + { + Ok(Some(data)) => data, + Ok(None) => { + return json_error( + StatusCode::BAD_REQUEST, + "invalid_request", + "Invalid or expired request_uri", + ); + } + Err(e) => { + return json_error( + StatusCode::INTERNAL_SERVER_ERROR, + "server_error", + &format!("Database error: {:?}", e), + ); + } + }; let flow_state = AuthFlowState::from_request_data(&request_data); if flow_state.is_expired() { - let _ = state.oauth_repo.delete_authorization_request(&consent_post_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&consent_post_request_id) + .await; return json_error( StatusCode::BAD_REQUEST, "invalid_request", @@ -1576,29 +1724,34 @@ pub async fn consent_post( }) .collect(); let consent_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); - let _ = state.oauth_repo.upsert_scope_preferences( - &did, - &consent_post_client_id, - &preferences, - ) - .await; + let _ = state + .oauth_repo + .upsert_scope_preferences(&did, &consent_post_client_id, &preferences) + .await; } - if let Err(e) = - state.oauth_repo.update_request_scope(&consent_post_request_id, &approved_scope_str).await + if let Err(e) = state + .oauth_repo + .update_request_scope(&consent_post_request_id, &approved_scope_str) + .await { tracing::warn!("Failed to update request scope: {:?}", e); } let code = Code::generate(); - let consent_post_device_id = request_data.device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); + let consent_post_device_id = request_data + .device_id + .as_ref() + .map(|d| DeviceIdType::from(d.clone())); let consent_post_code = AuthorizationCode::from(code.0.clone()); - if state.oauth_repo.update_authorization_request( - &consent_post_request_id, - &did, - consent_post_device_id.as_ref(), - &consent_post_code, - ) - .await - .is_err() + if state + .oauth_repo + .update_authorization_request( + &consent_post_request_id, + &did, + consent_post_device_id.as_ref(), + &consent_post_code, + ) + .await + .is_err() { return json_error( StatusCode::INTERNAL_SERVER_ERROR, @@ -1649,7 +1802,11 @@ pub async fn authorize_2fa_post( ); } let twofa_post_request_id = RequestId::from(form.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&twofa_post_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&twofa_post_request_id) + .await + { Ok(Some(d)) => d, Ok(None) => { return json_error( @@ -1667,20 +1824,25 @@ pub async fn authorize_2fa_post( } }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&twofa_post_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&twofa_post_request_id) + .await; return json_error( StatusCode::BAD_REQUEST, "invalid_request", "Authorization request has expired.", ); } - let challenge = state.oauth_repo.get_2fa_challenge(&twofa_post_request_id) + let challenge = state + .oauth_repo + .get_2fa_challenge(&twofa_post_request_id) .await .ok() .flatten(); if let Some(challenge) = challenge { if challenge.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_2fa_challenge( challenge.id).await; + let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; return json_error( StatusCode::BAD_REQUEST, "invalid_request", @@ -1688,7 +1850,7 @@ pub async fn authorize_2fa_post( ); } if challenge.attempts >= MAX_2FA_ATTEMPTS { - let _ = state.oauth_repo.delete_2fa_challenge( challenge.id).await; + let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; return json_error( StatusCode::FORBIDDEN, "access_denied", @@ -1714,14 +1876,16 @@ pub async fn authorize_2fa_post( let device_id = extract_device_cookie(&headers); let twofa_totp_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); let twofa_totp_code = AuthorizationCode::from(code.0.clone()); - if state.oauth_repo.update_authorization_request( - &twofa_post_request_id, - &challenge.did, - twofa_totp_device_id.as_ref(), - &twofa_totp_code, - ) - .await - .is_err() + if state + .oauth_repo + .update_authorization_request( + &twofa_post_request_id, + &challenge.did, + twofa_totp_device_id.as_ref(), + &twofa_totp_code, + ) + .await + .is_err() { return json_error( StatusCode::INTERNAL_SERVER_ERROR, @@ -1753,7 +1917,11 @@ pub async fn authorize_2fa_post( let did: tranquil_types::Did = match did_str.parse() { Ok(d) => d, Err(_) => { - return json_error(StatusCode::BAD_REQUEST, "invalid_request", "Invalid DID format."); + return json_error( + StatusCode::BAD_REQUEST, + "invalid_request", + "Invalid DID format.", + ); } }; if !crate::api::server::has_totp_enabled(&state, &did).await { @@ -1817,14 +1985,16 @@ pub async fn authorize_2fa_post( let code = Code::generate(); let twofa_final_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); let twofa_final_code = AuthorizationCode::from(code.0.clone()); - if state.oauth_repo.update_authorization_request( - &twofa_post_request_id, - &did, - twofa_final_device_id.as_ref(), - &twofa_final_code, - ) - .await - .is_err() + if state + .oauth_repo + .update_authorization_request( + &twofa_post_request_id, + &did, + twofa_final_device_id.as_ref(), + &twofa_final_code, + ) + .await + .is_err() { return json_error( StatusCode::INTERNAL_SERVER_ERROR, @@ -1935,7 +2105,13 @@ pub async fn check_user_security_status( .is_delegated_account(&u.did) .await .unwrap_or(false); - (passkeys, totp, has_pw, has_controllers, Some(u.did.to_string())) + ( + passkeys, + totp, + has_pw, + has_controllers, + Some(u.did.to_string()), + ) } _ => (false, false, false, false, None), }; @@ -1985,7 +2161,11 @@ pub async fn passkey_start( } let passkey_start_request_id = RequestId::from(form.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&passkey_start_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&passkey_start_request_id) + .await + { Ok(Some(data)) => data, Ok(None) => { return ( @@ -2010,7 +2190,10 @@ pub async fn passkey_start( }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&passkey_start_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&passkey_start_request_id) + .await; return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ @@ -2035,7 +2218,11 @@ pub async fn passkey_start( normalized_username.to_string() }; - let user = match state.user_repo.get_login_info_by_handle_or_email(&normalized_username).await { + let user = match state + .user_repo + .get_login_info_by_handle_or_email(&normalized_username) + .await + { Ok(Some(u)) => u, Ok(None) => { return ( @@ -2200,7 +2387,9 @@ pub async fn passkey_start( .into_response(); } - if state.oauth_repo.set_authorization_did(&passkey_start_request_id, &user.did, None) + if state + .oauth_repo + .set_authorization_did(&passkey_start_request_id, &user.did, None) .await .is_err() { @@ -2231,7 +2420,11 @@ pub async fn passkey_finish( Json(form): Json, ) -> Response { let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&passkey_finish_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&passkey_finish_request_id) + .await + { Ok(Some(data)) => data, Ok(None) => { return ( @@ -2256,7 +2449,10 @@ pub async fn passkey_finish( }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&passkey_finish_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&passkey_finish_request_id) + .await; return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ @@ -2434,13 +2630,26 @@ pub async fn passkey_finish( if let Ok(Some(user)) = user && user.two_factor_enabled { - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&passkey_finish_request_id).await; - match state.oauth_repo.create_2fa_challenge(&did, &passkey_finish_request_id).await { + let _ = state + .oauth_repo + .delete_2fa_challenge_by_request_uri(&passkey_finish_request_id) + .await; + match state + .oauth_repo + .create_2fa_challenge(&did, &passkey_finish_request_id) + .await + { Ok(challenge) => { let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); - if let Err(e) = - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &hostname).await + if let Err(e) = enqueue_2fa_code( + state.user_repo.as_ref(), + state.infra_repo.as_ref(), + user.id, + &challenge.code, + &hostname, + ) + .await { tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); } @@ -2496,14 +2705,16 @@ pub async fn passkey_finish( let code = Code::generate(); let passkey_final_device_id = device_id.as_ref().map(|d| DeviceIdType::from(d.clone())); let passkey_final_code = AuthorizationCode::from(code.0.clone()); - if state.oauth_repo.update_authorization_request( - &passkey_finish_request_id, - &did, - passkey_final_device_id.as_ref(), - &passkey_final_code, - ) - .await - .is_err() + if state + .oauth_repo + .update_authorization_request( + &passkey_finish_request_id, + &did, + passkey_final_device_id.as_ref(), + &passkey_final_code, + ) + .await + .is_err() { return ( StatusCode::INTERNAL_SERVER_ERROR, @@ -2547,7 +2758,11 @@ pub async fn authorize_passkey_start( let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let auth_passkey_start_request_id = RequestId::from(query.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&auth_passkey_start_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&auth_passkey_start_request_id) + .await + { Ok(Some(d)) => d, Ok(None) => { return ( @@ -2572,7 +2787,10 @@ pub async fn authorize_passkey_start( }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&auth_passkey_start_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&auth_passkey_start_request_id) + .await; return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ @@ -2719,7 +2937,11 @@ pub async fn authorize_passkey_finish( let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); - let request_data = match state.oauth_repo.get_authorization_request(&passkey_finish_request_id).await { + let request_data = match state + .oauth_repo + .get_authorization_request(&passkey_finish_request_id) + .await + { Ok(Some(d)) => d, Ok(None) => { return ( @@ -2744,7 +2966,10 @@ pub async fn authorize_passkey_finish( }; if request_data.expires_at < Utc::now() { - let _ = state.oauth_repo.delete_authorization_request(&passkey_finish_request_id).await; + let _ = state + .oauth_repo + .delete_authorization_request(&passkey_finish_request_id) + .await; return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ @@ -2809,18 +3034,19 @@ pub async fn authorize_passkey_finish( } }; - let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = - match serde_json::from_str(&auth_state_json) { - Ok(s) => s, - Err(e) => { - tracing::error!("Failed to deserialize authentication state: {:?}", e); - return ( + let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = match serde_json::from_str( + &auth_state_json, + ) { + Ok(s) => s, + Err(e) => { + tracing::error!("Failed to deserialize authentication state: {:?}", e); + return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), ) .into_response(); - } - }; + } + }; let credential: webauthn_rs::prelude::PublicKeyCredential = match serde_json::from_value(form.credential.clone()) { @@ -2892,7 +3118,11 @@ pub async fn authorize_passkey_finish( Ok(true) => {} } - let has_totp = state.user_repo.has_totp_enabled(&did).await.unwrap_or(false); + let has_totp = state + .user_repo + .has_totp_enabled(&did) + .await + .unwrap_or(false); if has_totp { let device_cookie = extract_device_cookie(&headers); let device_is_trusted = if let Some(ref dev_id) = device_cookie { @@ -2903,7 +3133,8 @@ pub async fn authorize_passkey_finish( if device_is_trusted { if let Some(ref dev_id) = device_cookie { - let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id).await; + let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) + .await; } } else { let user = match state.user_repo.get_2fa_status_by_did(&did).await { @@ -2917,11 +3148,24 @@ pub async fn authorize_passkey_finish( } }; - let _ = state.oauth_repo.delete_2fa_challenge_by_request_uri(&passkey_finish_request_id).await; - match state.oauth_repo.create_2fa_challenge(&did, &passkey_finish_request_id).await { + let _ = state + .oauth_repo + .delete_2fa_challenge_by_request_uri(&passkey_finish_request_id) + .await; + match state + .oauth_repo + .create_2fa_challenge(&did, &passkey_finish_request_id) + .await + { Ok(challenge) => { - if let Err(e) = - enqueue_2fa_code(state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, &challenge.code, &pds_hostname).await + if let Err(e) = enqueue_2fa_code( + state.user_repo.as_ref(), + state.infra_repo.as_ref(), + user.id, + &challenge.code, + &pds_hostname, + ) + .await { tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); } diff --git a/crates/tranquil-pds/src/oauth/endpoints/delegation.rs b/crates/tranquil-pds/src/oauth/endpoints/delegation.rs index 59a67a7..6436019 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/delegation.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/delegation.rs @@ -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, + headers: HeaderMap, + Json(form): Json, +) -> 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() +} diff --git a/crates/tranquil-pds/src/oauth/endpoints/par.rs b/crates/tranquil-pds/src/oauth/endpoints/par.rs index ace9459..d03eb89 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/par.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/par.rs @@ -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; diff --git a/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs b/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs index 4e73c12..0b3cf85 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs +++ b/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs @@ -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::() { - 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::() + && 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); } } }); diff --git a/crates/tranquil-pds/src/oauth/verify.rs b/crates/tranquil-pds/src/oauth/verify.rs index a8b41ee..3a4d956 100644 --- a/crates/tranquil-pds/src/oauth/verify.rs +++ b/crates/tranquil-pds/src/oauth/verify.rs @@ -385,7 +385,10 @@ struct LegacyAuthResult { did: String, } -async fn try_legacy_auth(user_repo: &dyn UserRepository, token: &str) -> Result { +async fn try_legacy_auth( + user_repo: &dyn UserRepository, + token: &str, +) -> Result { match crate::auth::validate_bearer_token(user_repo, token).await { Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did.to_string(), diff --git a/crates/tranquil-pds/src/plc/mod.rs b/crates/tranquil-pds/src/plc/mod.rs index 2e26912..94689c4 100644 --- a/crates/tranquil-pds/src/plc/mod.rs +++ b/crates/tranquil-pds/src/plc/mod.rs @@ -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) } diff --git a/crates/tranquil-pds/src/scheduled.rs b/crates/tranquil-pds/src/scheduled.rs index 20b36f7..b063053 100644 --- a/crates/tranquil-pds/src/scheduled.rs +++ b/crates/tranquil-pds/src/scheduled.rs @@ -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 { 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::>(), @@ -335,7 +342,8 @@ pub async fn backfill_record_blobs( repo_repo: Arc, 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!( diff --git a/crates/tranquil-pds/src/sync/blob.rs b/crates/tranquil-pds/src/sync/blob.rs index 52ff131..4543953 100644 --- a/crates/tranquil-pds/src/sync/blob.rs +++ b/crates/tranquil-pds/src/sync/blob.rs @@ -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); diff --git a/crates/tranquil-pds/src/sync/commit.rs b/crates/tranquil-pds/src/sync/commit.rs index 8dc4a6d..5a37bab 100644 --- a/crates/tranquil-pds/src/sync/commit.rs +++ b/crates/tranquil-pds/src/sync/commit.rs @@ -102,12 +102,12 @@ pub async fn list_repos( Query(params): Query, ) -> Response { let limit = params.limit.unwrap_or(50).clamp(1, 1000); - let cursor_did: Option = params - .cursor - .as_ref() - .and_then(|s| s.parse().ok()); + let cursor_did: Option = 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); diff --git a/crates/tranquil-pds/src/sync/deprecated.rs b/crates/tranquil-pds/src/sync/deprecated.rs index 1548600..f254415 100644 --- a/crates/tranquil-pds/src/sync/deprecated.rs +++ b/crates/tranquil-pds/src/sync/deprecated.rs @@ -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(); }; diff --git a/crates/tranquil-pds/src/sync/frame.rs b/crates/tranquil-pds/src/sync/frame.rs index b8570f2..0c4cc2e 100644 --- a/crates/tranquil-pds/src/sync/frame.rs +++ b/crates/tranquil-pds/src/sync/frame.rs @@ -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() } diff --git a/crates/tranquil-pds/src/sync/listener.rs b/crates/tranquil-pds/src/sync/listener.rs index 357abe9..ebf1c9e 100644 --- a/crates/tranquil-pds/src/sync/listener.rs +++ b/crates/tranquil-pds/src/sync/listener.rs @@ -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); diff --git a/crates/tranquil-pds/src/sync/repo.rs b/crates/tranquil-pds/src/sync/repo.rs index bc8a114..220eeaa 100644 --- a/crates/tranquil-pds/src/sync/repo.rs +++ b/crates/tranquil-pds/src/sync/repo.rs @@ -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) => { diff --git a/crates/tranquil-pds/src/sync/subscribe_repos.rs b/crates/tranquil-pds/src/sync/subscribe_repos.rs index 7d84f6e..bd091e6 100644 --- a/crates/tranquil-pds/src/sync/subscribe_repos.rs +++ b/crates/tranquil-pds/src/sync/subscribe_repos.rs @@ -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() diff --git a/crates/tranquil-pds/src/sync/util.rs b/crates/tranquil-pds/src/sync/util.rs index cd0b7cb..f0c864f 100644 --- a/crates/tranquil-pds/src/sync/util.rs +++ b/crates/tranquil-pds/src/sync/util.rs @@ -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 = 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 = block_cids_str .iter() .filter_map(|s| Cid::from_str(s).ok()) diff --git a/crates/tranquil-pds/src/sync/verify.rs b/crates/tranquil-pds/src/sync/verify.rs index f5529b2..a5058c1 100644 --- a/crates/tranquil-pds/src/sync/verify.rs +++ b/crates/tranquil-pds/src/sync/verify.rs @@ -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; diff --git a/crates/tranquil-pds/src/test_new_file.rs b/crates/tranquil-pds/src/test_new_file.rs new file mode 100644 index 0000000..7a7ffc7 --- /dev/null +++ b/crates/tranquil-pds/src/test_new_file.rs @@ -0,0 +1,3 @@ +pub fn test_function() -> &'static str { + "NEW_FILE_TEST_67890" +} diff --git a/crates/tranquil-pds/src/util.rs b/crates/tranquil-pds/src/util.rs index 781dce5..6991979 100644 --- a/crates/tranquil-pds/src/util.rs +++ b/crates/tranquil-pds/src/util.rs @@ -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] diff --git a/crates/tranquil-pds/src/validation/mod.rs b/crates/tranquil-pds/src/validation/mod.rs index 32ae6a9..586df12 100644 --- a/crates/tranquil-pds/src/validation/mod.rs +++ b/crates/tranquil-pds/src/validation/mod.rs @@ -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) -> Result<(), ValidationError> { + fn validate_post(obj: &serde_json::Map) -> 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, - ) -> Result<(), ValidationError> { + fn validate_profile(obj: &serde_json::Map) -> 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) -> Result<(), ValidationError> { + fn validate_like(obj: &serde_json::Map) -> 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) -> Result<(), ValidationError> { + fn validate_repost(obj: &serde_json::Map) -> 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) -> Result<(), ValidationError> { + fn validate_follow(obj: &serde_json::Map) -> 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) -> Result<(), ValidationError> { + fn validate_block(obj: &serde_json::Map) -> 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) -> Result<(), ValidationError> { + fn validate_list(obj: &serde_json::Map) -> 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, - ) -> Result<(), ValidationError> { + fn validate_list_item(obj: &serde_json::Map) -> 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, rkey: Option<&str>, ) -> Result<(), ValidationError> { @@ -364,10 +357,7 @@ impl RecordValidator { Ok(()) } - fn validate_starterpack( - &self, - obj: &serde_json::Map, - ) -> Result<(), ValidationError> { + fn validate_starterpack(obj: &serde_json::Map) -> 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, - ) -> Result<(), ValidationError> { + fn validate_threadgate(obj: &serde_json::Map) -> 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, ) -> 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()) diff --git a/crates/tranquil-pds/tests/commit_signing.rs b/crates/tranquil-pds/tests/commit_signing.rs index 7d1c765..5443cce 100644 --- a/crates/tranquil-pds/tests/commit_signing.rs +++ b/crates/tranquil-pds/tests/commit_signing.rs @@ -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()), }; diff --git a/crates/tranquil-pds/tests/did_web.rs b/crates/tranquil-pds/tests/did_web.rs index 074134c..8b43518 100644 --- a/crates/tranquil-pds/tests/did_web.rs +++ b/crates/tranquil-pds/tests/did_web.rs @@ -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, diff --git a/crates/tranquil-pds/tests/identity.rs b/crates/tranquil-pds/tests/identity.rs index 8822a11..610d6c9 100644 --- a/crates/tranquil-pds/tests/identity.rs +++ b/crates/tranquil-pds/tests/identity.rs @@ -51,10 +51,7 @@ async fn test_resolve_handle_not_found() { let _base = base_url().await; let params = [("handle", "nonexistent.handle.test")]; let res = client - .get(format!( - "{}/xrpc/com.atproto.identity.resolveHandle", - _base - )) + .get(format!("{}/xrpc/com.atproto.identity.resolveHandle", _base)) .query(¶ms) .send() .await @@ -149,10 +146,7 @@ async fn test_create_did_web_account_and_resolve() { "signingKey": signing_key }); let res = client - .post(format!( - "{}/xrpc/com.atproto.server.createAccount", - base - )) + .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) .json(&payload) .send() .await @@ -274,10 +268,7 @@ async fn test_did_web_lifecycle() { "signingKey": signing_key }); let res = client - .post(format!( - "{}/xrpc/com.atproto.server.createAccount", - base - )) + .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) .json(&create_payload) .send() .await diff --git a/crates/tranquil-pds/tests/import_with_verification.rs b/crates/tranquil-pds/tests/import_with_verification.rs index 69b97ab..18dab9b 100644 --- a/crates/tranquil-pds/tests/import_with_verification.rs +++ b/crates/tranquil-pds/tests/import_with_verification.rs @@ -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, 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"); diff --git a/crates/tranquil-pds/tests/verify_live_commit.rs b/crates/tranquil-pds/tests/verify_live_commit.rs index e96e82b..9df7965 100644 --- a/crates/tranquil-pds/tests/verify_live_commit.rs +++ b/crates/tranquil-pds/tests/verify_live_commit.rs @@ -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 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], diff --git a/crates/tranquil-repo/Cargo.toml b/crates/tranquil-repo/Cargo.toml index 77e2a16..e5c1cc2 100644 --- a/crates/tranquil-repo/Cargo.toml +++ b/crates/tranquil-repo/Cargo.toml @@ -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 } diff --git a/crates/tranquil-scopes/src/definitions.rs b/crates/tranquil-scopes/src/definitions.rs index aabdf84..d38a4b8 100644 --- a/crates/tranquil-scopes/src/definitions.rs +++ b/crates/tranquil-scopes/src/definitions.rs @@ -33,14 +33,14 @@ pub struct ScopeDefinition { pub display_name: &'static str, } -pub static SCOPE_DEFINITIONS: LazyLock> = - LazyLock::new(|| { +pub static SCOPE_DEFINITIONS: LazyLock> = 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> = ]; definitions.into_iter().map(|d| (d.scope, d)).collect() - }); + }, +); #[allow(dead_code)] pub fn get_scope_definition(scope: &str) -> Option<&'static ScopeDefinition> { diff --git a/crates/tranquil-scopes/src/parser.rs b/crates/tranquil-scopes/src/parser.rs index c392f6e..e83d773 100644 --- a/crates/tranquil-scopes/src/parser.rs +++ b/crates/tranquil-scopes/src/parser.rs @@ -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, } } diff --git a/crates/tranquil-scopes/src/permissions.rs b/crates/tranquil-scopes/src/permissions.rs index cbd5054..d30b686 100644 --- a/crates/tranquil-scopes/src/permissions.rs +++ b/crates/tranquil-scopes/src/permissions.rs @@ -9,7 +9,6 @@ use std::collections::HashSet; pub struct ScopePermissions { scopes: HashSet, parsed: Vec, - 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 { @@ -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")); + } } diff --git a/crates/tranquil-storage/Cargo.toml b/crates/tranquil-storage/Cargo.toml index d4a7cb2..feb6f5a 100644 --- a/crates/tranquil-storage/Cargo.toml +++ b/crates/tranquil-storage/Cargo.toml @@ -13,5 +13,3 @@ aws-sdk-s3 = { workspace = true } bytes = { workspace = true } futures = { workspace = true } sha2 = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } diff --git a/crates/tranquil-types/Cargo.toml b/crates/tranquil-types/Cargo.toml index eae451a..5f52c5f 100644 --- a/crates/tranquil-types/Cargo.toml +++ b/crates/tranquil-types/Cargo.toml @@ -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 } diff --git a/crates/tranquil-types/src/lib.rs b/crates/tranquil-types/src/lib.rs index ee877e9..5d2124f 100644 --- a/crates/tranquil-types/src/lib.rs +++ b/crates/tranquil-types/src/lib.rs @@ -4,58 +4,198 @@ use std::fmt; use std::ops::Deref; use std::str::FromStr; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct Did(String); +macro_rules! impl_string_common { + ($name:ident) => { + impl $name { + pub fn as_str(&self) -> &str { + &self.0 + } -impl<'de> Deserialize<'de> for Did { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Did::new(&s).map_err(|e| serde::de::Error::custom(e.to_string())) - } + pub fn into_inner(self) -> String { + self.0 + } + } + + impl AsRef for $name { + fn as_ref(&self) -> &str { + &self.0 + } + } + + impl Deref for $name { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl From<$name> for String { + fn from(val: $name) -> Self { + val.0 + } + } + + impl From for $name { + fn from(s: String) -> Self { + Self(s) + } + } + + impl<'a> From<&'a $name> for Cow<'a, str> { + fn from(val: &'a $name) -> Self { + Cow::Borrowed(&val.0) + } + } + + impl PartialEq for $name { + fn eq(&self, other: &str) -> bool { + self.0 == other + } + } + + impl PartialEq<&str> for $name { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } + } + + impl PartialEq for $name { + fn eq(&self, other: &String) -> bool { + self.0 == *other + } + } + + impl PartialEq<$name> for String { + fn eq(&self, other: &$name) -> bool { + *self == other.0 + } + } + + impl PartialEq<$name> for &str { + fn eq(&self, other: &$name) -> bool { + *self == other.0 + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } + } + }; } -impl From for String { - fn from(did: Did) -> Self { - did.0 - } +macro_rules! simple_string_newtype { + ( + $(#[$meta:meta])* + $vis:vis struct $name:ident; + ) => { + $(#[$meta])* + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] + #[serde(transparent)] + #[sqlx(transparent)] + $vis struct $name(String); + + impl $name { + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + pub fn new_unchecked(s: impl Into) -> Self { + Self(s.into()) + } + } + + impl_string_common!($name); + }; } -impl From for Did { - fn from(s: String) -> Self { - Did(s) - } +macro_rules! simple_string_newtype_no_sqlx { + ( + $(#[$meta:meta])* + $vis:vis struct $name:ident; + ) => { + $(#[$meta])* + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[serde(transparent)] + $vis struct $name(String); + + impl $name { + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + pub fn new_unchecked(s: impl Into) -> Self { + Self(s.into()) + } + } + + impl_string_common!($name); + }; } -impl<'a> From<&'a Did> for Cow<'a, str> { - fn from(did: &'a Did) -> Self { - Cow::Borrowed(&did.0) - } +macro_rules! validated_string_newtype { + ( + $(#[$meta:meta])* + $vis:vis struct $name:ident; + error = $error:ident; + validator = $validator:expr; + ) => { + $(#[$meta])* + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] + #[serde(transparent)] + #[sqlx(transparent)] + $vis struct $name(String); + + impl<'de> Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + $name::new(&s).map_err(|e| serde::de::Error::custom(e.to_string())) + } + } + + impl $name { + pub fn new(s: impl Into) -> Result { + let s = s.into(); + let validator: fn(&str) -> Result<(), ()> = $validator; + validator(&s).map_err(|_| $error::Invalid(s.clone()))?; + Ok(Self(s)) + } + + pub fn new_unchecked(s: impl Into) -> Self { + Self(s.into()) + } + } + + impl FromStr for $name { + type Err = $error; + + fn from_str(s: &str) -> Result { + Self::new(s) + } + } + + impl_string_common!($name); + + #[derive(Debug, Clone, thiserror::Error)] + pub enum $error { + #[error("invalid: {0}")] + Invalid(String), + } + }; +} + +validated_string_newtype! { + pub struct Did; + error = DidError; + validator = |s| jacquard_common::types::string::Did::new(s).map(|_| ()).map_err(|_| ()); } impl Did { - pub fn new(s: impl Into) -> Result { - let s = s.into(); - jacquard::types::string::Did::new(&s).map_err(|_| DidError::Invalid(s.clone()))?; - Ok(Self(s)) - } - - pub fn new_unchecked(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } - pub fn is_plc(&self) -> bool { self.0.starts_with("did:plc:") } @@ -65,161 +205,32 @@ impl Did { } } -impl AsRef for Did { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl PartialEq for Did { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for Did { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for Did { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &Did) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Did) -> bool { - *self == other.0 - } -} - -impl Deref for Did { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for Did { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for Did { - type Err = DidError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum DidError { - #[error("invalid DID: {0}")] - Invalid(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] pub struct Handle(String); -impl From for String { - fn from(handle: Handle) -> Self { - handle.0 - } -} - -impl From for Handle { - fn from(s: String) -> Self { - Handle(s) - } -} - -impl<'a> From<&'a Handle> for Cow<'a, str> { - fn from(handle: &'a Handle) -> Self { - Cow::Borrowed(&handle.0) +impl<'de> Deserialize<'de> for Handle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(Handle(s)) } } impl Handle { pub fn new(s: impl Into) -> Result { let s = s.into(); - jacquard::types::string::Handle::new(&s).map_err(|_| HandleError::Invalid(s.clone()))?; + jacquard_common::types::string::Handle::new(&s) + .map_err(|_| HandleError::Invalid(s.clone()))?; Ok(Self(s)) } pub fn new_unchecked(s: impl Into) -> Self { Self(s.into()) } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for Handle { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for Handle { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for Handle { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for Handle { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for Handle { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &Handle) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Handle) -> bool { - *self == other.0 - } -} - -impl fmt::Display for Handle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } } impl FromStr for Handle { @@ -230,6 +241,8 @@ impl FromStr for Handle { } } +impl_string_common!(Handle); + #[derive(Debug, Clone, thiserror::Error)] pub enum HandleError { #[error("invalid handle: {0}")] @@ -325,14 +338,6 @@ impl fmt::Display for AtIdentifier { } } -impl FromStr for AtIdentifier { - type Err = AtIdentifierError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - impl Serialize for AtIdentifier { fn serialize(&self, serializer: S) -> Result where @@ -358,288 +363,54 @@ pub enum AtIdentifierError { Invalid(String), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(type_name = "rkey")] -pub struct Rkey(String); - -impl From for String { - fn from(rkey: Rkey) -> Self { - rkey.0 - } -} - -impl From for Rkey { - fn from(s: String) -> Self { - Rkey(s) - } -} - -impl<'a> From<&'a Rkey> for Cow<'a, str> { - fn from(rkey: &'a Rkey) -> Self { - Cow::Borrowed(&rkey.0) - } +validated_string_newtype! { + pub struct Rkey; + error = RkeyError; + validator = |s| jacquard_common::types::string::Rkey::new(s).map(|_| ()).map_err(|_| ()); } impl Rkey { - pub fn new(s: impl Into) -> Result { - let s = s.into(); - jacquard::types::string::Rkey::new(&s).map_err(|_| RkeyError::Invalid(s.clone()))?; - Ok(Self(s)) - } - - pub fn new_unchecked(s: impl Into) -> Self { - Self(s.into()) - } - pub fn generate() -> Self { - use jacquard::types::integer::LimitedU32; - Self(jacquard::types::string::Tid::now(LimitedU32::MIN).to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 + use jacquard_common::types::integer::LimitedU32; + Self(jacquard_common::types::string::Tid::now(LimitedU32::MIN).to_string()) } pub fn is_tid(&self) -> bool { - jacquard::types::string::Tid::from_str(&self.0).is_ok() + Tid::new(&self.0).is_ok() + } + + pub fn to_tid(&self) -> Option { + Tid::new(&self.0).ok() } } -impl AsRef for Rkey { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for Rkey { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for Rkey { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for Rkey { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for Rkey { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &Rkey) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Rkey) -> bool { - *self == other.0 - } -} - -impl fmt::Display for Rkey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for Rkey { - type Err = RkeyError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum RkeyError { - #[error("invalid rkey: {0}")] - Invalid(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(type_name = "nsid")] -pub struct Nsid(String); - -impl From for String { - fn from(nsid: Nsid) -> Self { - nsid.0 - } -} - -impl From for Nsid { - fn from(s: String) -> Self { - Nsid(s) - } -} - -impl<'a> From<&'a Nsid> for Cow<'a, str> { - fn from(nsid: &'a Nsid) -> Self { - Cow::Borrowed(&nsid.0) - } +validated_string_newtype! { + pub struct Nsid; + error = NsidError; + validator = |s| jacquard_common::types::string::Nsid::new(s).map(|_| ()).map_err(|_| ()); } impl Nsid { - pub fn new(s: impl Into) -> Result { - let s = s.into(); - jacquard::types::string::Nsid::new(&s).map_err(|_| NsidError::Invalid(s.clone()))?; - Ok(Self(s)) + pub fn authority(&self) -> &str { + self.0.split('.').rev().nth(1).unwrap_or("") } - pub fn new_unchecked(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } - - pub fn authority(&self) -> Option<&str> { - let parts: Vec<&str> = self.0.rsplitn(2, '.').collect(); - if parts.len() == 2 { - Some(parts[1]) - } else { - None - } - } - - pub fn name(&self) -> Option<&str> { - self.0.rsplit('.').next() + pub fn name(&self) -> &str { + self.0.split('.').next_back().unwrap_or("") } } -impl AsRef for Nsid { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for Nsid { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for Nsid { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for Nsid { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for Nsid { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &Nsid) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Nsid) -> bool { - *self == other.0 - } -} - -impl fmt::Display for Nsid { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for Nsid { - type Err = NsidError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum NsidError { - #[error("invalid NSID: {0}")] - Invalid(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(type_name = "at_uri")] -pub struct AtUri(String); - -impl From for String { - fn from(uri: AtUri) -> Self { - uri.0 - } -} - -impl From for AtUri { - fn from(s: String) -> Self { - AtUri(s) - } -} - -impl<'a> From<&'a AtUri> for Cow<'a, str> { - fn from(uri: &'a AtUri) -> Self { - Cow::Borrowed(&uri.0) - } +validated_string_newtype! { + pub struct AtUri; + error = AtUriError; + validator = |s| jacquard_common::types::string::AtUri::new(s).map(|_| ()).map_err(|_| ()); } impl AtUri { - pub fn new(s: impl Into) -> Result { - let s = s.into(); - jacquard::types::string::AtUri::new(&s).map_err(|_| AtUriError::Invalid(s.clone()))?; - Ok(Self(s)) - } - - pub fn new_unchecked(s: impl Into) -> Self { - Self(s.into()) - } - pub fn from_parts(did: &str, collection: &str, rkey: &str) -> Self { Self(format!("at://{}/{}/{}", did, collection, rkey)) } - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } - pub fn did(&self) -> Option<&str> { self.0 .strip_prefix("at://") @@ -659,426 +430,55 @@ impl AtUri { } } -impl AsRef for AtUri { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for AtUri { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for AtUri { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for AtUri { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for AtUri { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &AtUri) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &AtUri) -> bool { - *self == other.0 - } -} - -impl fmt::Display for AtUri { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for AtUri { - type Err = AtUriError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum AtUriError { - #[error("invalid AT URI: {0}")] - Invalid(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct Tid(String); - -impl From for String { - fn from(tid: Tid) -> Self { - tid.0 - } -} - -impl From for Tid { - fn from(s: String) -> Self { - Tid(s) - } -} - -impl<'a> From<&'a Tid> for Cow<'a, str> { - fn from(tid: &'a Tid) -> Self { - Cow::Borrowed(&tid.0) - } +validated_string_newtype! { + pub struct Tid; + error = TidError; + validator = |s| jacquard_common::types::string::Tid::from_str(s).map(|_| ()).map_err(|_| ()); } impl Tid { - pub fn new(s: impl Into) -> Result { - let s = s.into(); - jacquard::types::string::Tid::from_str(&s).map_err(|_| TidError::Invalid(s.clone()))?; - Ok(Self(s)) - } - - pub fn new_unchecked(s: impl Into) -> Self { - Self(s.into()) - } - pub fn now() -> Self { - use jacquard::types::integer::LimitedU32; - Self(jacquard::types::string::Tid::now(LimitedU32::MIN).to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 + use jacquard_common::types::integer::LimitedU32; + Self(jacquard_common::types::string::Tid::now(LimitedU32::MIN).to_string()) } } -impl AsRef for Tid { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for Tid { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for Tid { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for Tid { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for Tid { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &Tid) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Tid) -> bool { - *self == other.0 - } -} - -impl fmt::Display for Tid { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for Tid { - type Err = TidError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum TidError { - #[error("invalid TID: {0}")] - Invalid(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct Datetime(String); - -impl From for String { - fn from(dt: Datetime) -> Self { - dt.0 - } -} - -impl From for Datetime { - fn from(s: String) -> Self { - Datetime(s) - } -} - -impl<'a> From<&'a Datetime> for Cow<'a, str> { - fn from(dt: &'a Datetime) -> Self { - Cow::Borrowed(&dt.0) - } +validated_string_newtype! { + pub struct Datetime; + error = DatetimeError; + validator = |s| jacquard_common::types::string::Datetime::from_str(s).map(|_| ()).map_err(|_| ()); } impl Datetime { - pub fn new(s: impl Into) -> Result { - let s = s.into(); - jacquard::types::string::Datetime::from_str(&s) - .map_err(|_| DatetimeError::Invalid(s.clone()))?; - Ok(Self(s)) - } - - pub fn new_unchecked(s: impl Into) -> Self { - Self(s.into()) - } - pub fn now() -> Self { - Self( - chrono::Utc::now() - .format("%Y-%m-%dT%H:%M:%S%.3fZ") - .to_string(), - ) + Self(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)) } - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 + pub fn to_chrono(&self) -> Option> { + chrono::DateTime::parse_from_rfc3339(&self.0) + .ok() + .map(|dt| dt.with_timezone(&chrono::Utc)) } } -impl AsRef for Datetime { - fn as_ref(&self) -> &str { - &self.0 - } +validated_string_newtype! { + pub struct Language; + error = LanguageError; + validator = |s| jacquard_common::types::string::Language::from_str(s).map(|_| ()).map_err(|_| ()); } -impl Deref for Datetime { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for Datetime { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for Datetime { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for Datetime { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &Datetime) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Datetime) -> bool { - *self == other.0 - } -} - -impl fmt::Display for Datetime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for Datetime { - type Err = DatetimeError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum DatetimeError { - #[error("invalid datetime: {0}")] - Invalid(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct Language(String); - -impl From for String { - fn from(lang: Language) -> Self { - lang.0 - } -} - -impl From for Language { - fn from(s: String) -> Self { - Language(s) - } -} - -impl<'a> From<&'a Language> for Cow<'a, str> { - fn from(lang: &'a Language) -> Self { - Cow::Borrowed(&lang.0) - } -} - -impl Language { - pub fn new(s: impl Into) -> Result { - let s = s.into(); - jacquard::types::string::Language::from_str(&s) - .map_err(|_| LanguageError::Invalid(s.clone()))?; - Ok(Self(s)) - } - - pub fn new_unchecked(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for Language { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for Language { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for Language { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for Language { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for Language { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &Language) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Language) -> bool { - *self == other.0 - } -} - -impl fmt::Display for Language { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for Language { - type Err = LanguageError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum LanguageError { - #[error("invalid language tag: {0}")] - Invalid(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] pub struct CidLink(String); -impl From for String { - fn from(cid: CidLink) -> Self { - cid.0 - } -} - -impl From for CidLink { - fn from(s: String) -> Self { - CidLink(s) - } -} - -impl<'a> From<&'a CidLink> for Cow<'a, str> { - fn from(cid: &'a CidLink) -> Self { - Cow::Borrowed(&cid.0) +impl<'de> Deserialize<'de> for CidLink { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + CidLink::new(&s).map_err(|e| serde::de::Error::custom(e.to_string())) } } @@ -1093,66 +493,8 @@ impl CidLink { Self(s.into()) } - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } - - pub fn to_cid(&self) -> Result { - cid::Cid::from_str(&self.0) - } -} - -impl AsRef for CidLink { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for CidLink { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for CidLink { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq<&str> for CidLink { - fn eq(&self, other: &&str) -> bool { - self.0 == *other - } -} - -impl PartialEq for CidLink { - fn eq(&self, other: &String) -> bool { - self.0 == *other - } -} - -impl PartialEq for String { - fn eq(&self, other: &CidLink) -> bool { - *self == other.0 - } -} - -impl PartialEq for &str { - fn eq(&self, other: &CidLink) -> bool { - *self == other.0 - } -} - -impl fmt::Display for CidLink { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + pub fn to_cid(&self) -> Option { + cid::Cid::from_str(&self.0).ok() } } @@ -1164,6 +506,8 @@ impl FromStr for CidLink { } } +impl_string_common!(CidLink); + #[derive(Debug, Clone, thiserror::Error)] pub enum CidLinkError { #[error("invalid CID: {0}")] @@ -1264,7 +608,7 @@ impl fmt::Display for AccountState { } } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(transparent)] pub struct PlainPassword(String); @@ -1306,23 +650,19 @@ impl Deref for PlainPassword { } } -#[derive(Debug, Clone, Serialize, sqlx::Type)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] #[sqlx(transparent)] pub struct PasswordHash(String); impl PasswordHash { - pub fn from_hash(hash: impl Into) -> Self { - Self(hash.into()) + pub fn new(s: impl Into) -> Self { + Self(s.into()) } pub fn as_str(&self) -> &str { &self.0 } - - pub fn into_inner(self) -> String { - self.0 - } } impl AsRef for PasswordHash { @@ -1331,22 +671,15 @@ impl AsRef for PasswordHash { } } -impl From for PasswordHash { - fn from(s: String) -> Self { - Self(s) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum TokenSource { + #[serde(rename = "session")] Session, - OAuth { - client_id: Option, - }, - ServiceAuth { - lxm: Option, - aud: Option, - }, + #[serde(rename = "oauth")] + OAuth { client_id: ClientId, scope: String }, + #[serde(rename = "service_auth")] + ServiceAuth { aud: Did, lxm: Option }, } impl TokenSource { @@ -1363,679 +696,92 @@ impl TokenSource { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct JwkThumbprint(String); +simple_string_newtype_no_sqlx! { + pub struct JwkThumbprint; +} -impl JwkThumbprint { - pub fn new(s: impl Into) -> Self { - Self(s.into()) +simple_string_newtype_no_sqlx! { + pub struct DPoPProofId; +} + +simple_string_newtype! { + pub struct TokenId; +} + +simple_string_newtype! { + pub struct ClientId; +} + +simple_string_newtype! { + pub struct DeviceId; +} + +simple_string_newtype! { + pub struct RequestId; +} + +simple_string_newtype! { + pub struct Jti; +} + +simple_string_newtype! { + pub struct AuthorizationCode; +} + +simple_string_newtype! { + pub struct RefreshToken; +} + +simple_string_newtype! { + pub struct InviteCode; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "comms_channel", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] +#[derive(Copy)] +pub enum CommsChannel { + Email, + Discord, + Telegram, + Signal, +} + +impl CommsChannel { + pub fn as_str(&self) -> &'static str { + match self { + CommsChannel::Email => "email", + CommsChannel::Discord => "discord", + CommsChannel::Telegram => "telegram", + CommsChannel::Signal => "signal", + } } - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 + pub fn from_str_opt(s: &str) -> Option { + match s { + "email" => Some(CommsChannel::Email), + "discord" => Some(CommsChannel::Discord), + "telegram" => Some(CommsChannel::Telegram), + "signal" => Some(CommsChannel::Signal), + _ => None, + } } } -impl AsRef for JwkThumbprint { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for JwkThumbprint { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for JwkThumbprint { - fn from(s: String) -> Self { - Self(s) - } -} - -impl PartialEq for JwkThumbprint { - fn eq(&self, other: &str) -> bool { - self.0 == other - } -} - -impl PartialEq for JwkThumbprint { - fn eq(&self, other: &String) -> bool { - &self.0 == other - } -} - -impl PartialEq for String { - fn eq(&self, other: &JwkThumbprint) -> bool { - self == &other.0 - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct DPoPProofId(String); - -impl DPoPProofId { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for DPoPProofId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for DPoPProofId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for DPoPProofId { - fn from(s: String) -> Self { - Self(s) +impl fmt::Display for CommsChannel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct TokenId(String); - -impl TokenId { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for TokenId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for TokenId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for TokenId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for TokenId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct ClientId(String); - -impl ClientId { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for ClientId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for ClientId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for ClientId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for ClientId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct DeviceId(String); - -impl DeviceId { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for DeviceId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for DeviceId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for DeviceId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for DeviceId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct RequestId(String); - -impl RequestId { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for RequestId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for RequestId { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for RequestId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for RequestId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct Jti(String); - -impl Jti { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for Jti { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for Jti { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for Jti { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for Jti { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct AuthorizationCode(String); - -impl AuthorizationCode { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for AuthorizationCode { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for AuthorizationCode { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for AuthorizationCode { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for AuthorizationCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct RefreshToken(String); - -impl RefreshToken { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for RefreshToken { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for RefreshToken { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for RefreshToken { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for RefreshToken { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct InviteCode(String); - -impl InviteCode { - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_inner(self) -> String { - self.0 - } -} - -impl AsRef for InviteCode { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for InviteCode { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for InviteCode { - fn from(s: String) -> Self { - Self(s) - } -} - -impl fmt::Display for InviteCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_did_validation() { - assert!(Did::new("did:plc:abc123").is_ok()); - assert!(Did::new("did:web:example.com").is_ok()); - assert!(Did::new("not-a-did").is_err()); - assert!(Did::new("").is_err()); - } - - #[test] - fn test_did_methods() { - let plc = Did::new("did:plc:abc123").unwrap(); - assert!(plc.is_plc()); - assert!(!plc.is_web()); - assert_eq!(plc.as_str(), "did:plc:abc123"); - - let web = Did::new("did:web:example.com").unwrap(); - assert!(!web.is_plc()); - assert!(web.is_web()); - } - - #[test] - fn test_did_conversions() { - let did = Did::new("did:plc:test123").unwrap(); - let s: String = did.clone().into(); - assert_eq!(s, "did:plc:test123"); - assert_eq!(format!("{}", did), "did:plc:test123"); - } - - #[test] - fn test_did_serde() { - let did = Did::new("did:plc:test123").unwrap(); - let json = serde_json::to_string(&did).unwrap(); - assert_eq!(json, "\"did:plc:test123\""); - - let parsed: Did = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed, did); - } - - #[test] - fn test_handle_validation() { - assert!(Handle::new("user.bsky.social").is_ok()); - assert!(Handle::new("test.example.com").is_ok()); - assert!(Handle::new("invalid handle with spaces").is_err()); - assert!(Handle::new("alice.pds.test").is_ok()); - } - - #[test] - fn test_rkey_validation() { - assert!(Rkey::new("self").is_ok()); - assert!(Rkey::new("3jzfcijpj2z2a").is_ok()); - assert!(Rkey::new("invalid/rkey").is_err()); - } - - #[test] - fn test_rkey_generate() { - let rkey = Rkey::generate(); - assert!(rkey.is_tid()); - assert!(!rkey.as_str().is_empty()); - } - - #[test] - fn test_nsid_validation() { - assert!(Nsid::new("app.bsky.feed.post").is_ok()); - assert!(Nsid::new("com.atproto.repo.createRecord").is_ok()); - assert!(Nsid::new("invalid").is_err()); - } - - #[test] - fn test_nsid_parts() { - let nsid = Nsid::new("app.bsky.feed.post").unwrap(); - assert_eq!(nsid.name(), Some("post")); - } - - #[test] - fn test_at_uri_validation() { - assert!(AtUri::new("at://did:plc:abc123/app.bsky.feed.post/xyz").is_ok()); - assert!(AtUri::new("not-an-at-uri").is_err()); - } - - #[test] - fn test_at_uri_from_parts() { - let uri = AtUri::from_parts("did:plc:abc123", "app.bsky.feed.post", "xyz"); - assert_eq!(uri.as_str(), "at://did:plc:abc123/app.bsky.feed.post/xyz"); - } - - #[test] - fn test_type_safety() { - fn takes_did(_: &Did) {} - fn takes_handle(_: &Handle) {} - - let did = Did::new("did:plc:test").unwrap(); - let handle = Handle::new("test.bsky.social").unwrap(); - - takes_did(&did); - takes_handle(&handle); - } - - #[test] - fn test_tid_validation() { - let tid = Tid::now(); - assert!(!tid.as_str().is_empty()); - assert!(Tid::new(tid.as_str()).is_ok()); - assert!(Tid::new("invalid").is_err()); - } - - #[test] - fn test_datetime_validation() { - assert!(Datetime::new("2024-01-15T12:30:45.123Z").is_ok()); - assert!(Datetime::new("not-a-date").is_err()); - let now = Datetime::now(); - assert!(!now.as_str().is_empty()); - } - - #[test] - fn test_language_validation() { - assert!(Language::new("en").is_ok()); - assert!(Language::new("en-US").is_ok()); - assert!(Language::new("ja").is_ok()); - } - - #[test] - fn test_cidlink_validation() { - assert!( - CidLink::new("bafyreib74ckyq525l3y6an5txykwwtb3dgex6ofzakml53di77oxwr5pfe").is_ok() - ); - assert!(CidLink::new("not-a-cid").is_err()); - } - - #[test] - fn test_at_identifier_validation() { - let did_ident = AtIdentifier::new("did:plc:abc123").unwrap(); - assert!(did_ident.is_did()); - assert!(!did_ident.is_handle()); - assert!(did_ident.as_did().is_some()); - assert!(did_ident.as_handle().is_none()); - - let handle_ident = AtIdentifier::new("user.bsky.social").unwrap(); - assert!(!handle_ident.is_did()); - assert!(handle_ident.is_handle()); - assert!(handle_ident.as_did().is_none()); - assert!(handle_ident.as_handle().is_some()); - - assert!(AtIdentifier::new("invalid identifier").is_err()); - } - - #[test] - fn test_at_identifier_serde() { - let ident = AtIdentifier::new("did:plc:test123").unwrap(); - let json = serde_json::to_string(&ident).unwrap(); - assert_eq!(json, "\"did:plc:test123\""); - - let parsed: AtIdentifier = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed.as_str(), "did:plc:test123"); - } - - #[test] - fn test_account_state_active() { - let state = AccountState::from_db_fields(None, None, None, None); - assert!(state.is_active()); - assert!(!state.is_deactivated()); - assert!(!state.is_takendown()); - assert!(!state.is_migrated()); - assert!(state.can_login()); - assert!(state.can_access_repo()); - assert_eq!(state.status_string(), "active"); - } - - #[test] - fn test_account_state_deactivated() { - let now = chrono::Utc::now(); - let state = AccountState::from_db_fields(Some(now), None, None, None); - assert!(!state.is_active()); - assert!(state.is_deactivated()); - assert!(!state.is_takendown()); - assert!(!state.is_migrated()); - assert!(!state.can_login()); - assert!(state.can_access_repo()); - assert_eq!(state.status_string(), "deactivated"); - } - - #[test] - fn test_account_state_takendown() { - let state = AccountState::from_db_fields(None, Some("mod-action-123".into()), None, None); - assert!(!state.is_active()); - assert!(!state.is_deactivated()); - assert!(state.is_takendown()); - assert!(!state.is_migrated()); - assert!(!state.can_login()); - assert!(!state.can_access_repo()); - assert_eq!(state.status_string(), "takendown"); - } - - #[test] - fn test_account_state_migrated() { - let now = chrono::Utc::now(); - let state = - AccountState::from_db_fields(Some(now), None, Some("https://other.pds".into()), None); - assert!(!state.is_active()); - assert!(!state.is_deactivated()); - assert!(!state.is_takendown()); - assert!(state.is_migrated()); - assert!(!state.can_login()); - assert!(!state.can_access_repo()); - assert_eq!(state.status_string(), "deactivated"); - } - - #[test] - fn test_account_state_takedown_priority() { - let now = chrono::Utc::now(); - let state = AccountState::from_db_fields( - Some(now), - Some("mod-action".into()), - Some("https://other.pds".into()), - None, - ); - assert!(state.is_takendown()); - } +#[sqlx(type_name = "comms_type", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum CommsType { + Verification, + PasswordReset, + AccountDeleted, + AccountMigrated, + PasskeyRecovery, + MigrationVerification, } diff --git a/frontend/src/lib/oauth.ts b/frontend/src/lib/oauth.ts index ef15c2c..1ff22bf 100644 --- a/frontend/src/lib/oauth.ts +++ b/frontend/src/lib/oauth.ts @@ -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 { +export async function startOAuthLogin(loginHint?: string): Promise { clearAllOAuthState(); const state = generateState(); @@ -271,19 +273,24 @@ export async function startOAuthLogin(): Promise { saveOAuthState({ state, codeVerifier }); + const parParams: Record = { + 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 { + const keyPair = await getOrCreateDPoPKeyPair(); + const tokenHash = await sha256(accessToken); + const ath = base64UrlEncode(tokenHash); + return createDPoPProof(keyPair, method, url, getDPoPNonce() ?? undefined, ath); +} diff --git a/frontend/src/lib/registration/flow.svelte.ts b/frontend/src/lib/registration/flow.svelte.ts index f9fb066..4d95ee6 100644 --- a/frontend/src/lib/registration/flow.svelte.ts +++ b/frontend/src/lib/registration/flow.svelte.ts @@ -18,6 +18,11 @@ import type { RegistrationStep, SessionState, } from "./types.ts"; +import { + saveRegistrationState, + loadRegistrationState, + clearRegistrationState, +} from "./storage.ts"; export interface RegistrationFlowState { mode: RegistrationMode; @@ -76,6 +81,20 @@ export function createRegistrationFlow( return did.replace("did:web:", "").replace(/%3A/g, ":"); } + function persistState() { + if (state.step !== "info" && state.step !== "creating") { + saveRegistrationState( + state.mode, + state.step, + state.pdsHostname, + state.info, + state.externalDidWeb, + state.account, + state.session, + ); + } + } + function setError(err: unknown) { if (err instanceof ApiError) { state.error = err.message || "An error occurred"; @@ -129,6 +148,7 @@ export function createRegistrationFlow( "\t", ); state.step = "initial-did-doc"; + persistState(); } catch (err) { setError(err); } finally { @@ -184,6 +204,7 @@ export function createRegistrationFlow( handle: result.handle, }; state.step = "verify"; + persistState(); } catch (err) { setError(err); } finally { @@ -237,6 +258,7 @@ export function createRegistrationFlow( setupToken: result.setupToken, }; state.step = "passkey"; + persistState(); } catch (err) { setError(err); } finally { @@ -250,10 +272,12 @@ export function createRegistrationFlow( state.account.appPasswordName = appPasswordName; } state.step = "app-password"; + persistState(); } function proceedFromAppPassword() { state.step = "verify"; + persistState(); } async function verifyAccount(code: string) { @@ -296,6 +320,7 @@ export function createRegistrationFlow( "\t", ); state.step = "updated-did-doc"; + persistState(); } else { await api.activateAccount(session.accessJwt); await finalizeSession(); @@ -349,6 +374,7 @@ export function createRegistrationFlow( async function finalizeSession() { if (!state.session || !state.account) return; + clearRegistrationState(); setSession({ did: state.account.did, handle: state.account.handle, @@ -404,3 +430,22 @@ export function createRegistrationFlow( } export type RegistrationFlow = ReturnType; + +export function restoreRegistrationFlow(): RegistrationFlow | null { + const saved = loadRegistrationState(); + if (!saved || saved.step === "info" || saved.step === "redirect-to-dashboard") { + return null; + } + + const flow = createRegistrationFlow(saved.mode, saved.pdsHostname); + + flow.state.step = saved.step; + flow.state.info = { ...flow.state.info, ...saved.info }; + flow.state.externalDidWeb = { ...flow.state.externalDidWeb, ...saved.externalDidWeb }; + flow.state.account = saved.account; + flow.state.session = saved.session; + + return flow; +} + +export { hasPendingRegistration, getRegistrationResumeInfo, clearRegistrationState } from "./storage.ts"; diff --git a/frontend/src/lib/registration/storage.ts b/frontend/src/lib/registration/storage.ts new file mode 100644 index 0000000..58d5a70 --- /dev/null +++ b/frontend/src/lib/registration/storage.ts @@ -0,0 +1,195 @@ +import type { + RegistrationMode, + RegistrationStep, + RegistrationInfo, + ExternalDidWebState, + AccountResult, + SessionState, +} from "./types.ts"; + +const STORAGE_KEY = "tranquil_registration_state"; +const MAX_AGE_MS = 60 * 60 * 1000; + +interface StoredRegistrationState { + version: 1; + startedAt: string; + mode: RegistrationMode; + step: RegistrationStep; + pdsHostname: string; + info: RegistrationInfo; + externalDidWeb: StoredExternalDidWebState; + account: StoredAccountResult | null; + session: StoredSessionState | null; +} + +interface StoredExternalDidWebState { + keyMode: "reserved" | "byod"; + reservedSigningKey?: string; + byodPrivateKeyBase64?: string; + byodPublicKeyMultibase?: string; + initialDidDocument?: string; + updatedDidDocument?: string; +} + +interface StoredAccountResult { + did: string; + handle: string; + setupToken?: string; + appPassword?: string; + appPasswordName?: string; +} + +interface StoredSessionState { + accessJwt: string; + refreshJwt: string; +} + +function uint8ArrayToBase64(arr: Uint8Array): string { + return btoa(Array.from(arr, (byte) => String.fromCharCode(byte)).join("")); +} + +function base64ToUint8Array(base64: string): Uint8Array { + const binary = atob(base64); + return Uint8Array.from(binary, (char) => char.charCodeAt(0)); +} + +export function saveRegistrationState( + mode: RegistrationMode, + step: RegistrationStep, + pdsHostname: string, + info: RegistrationInfo, + externalDidWeb: ExternalDidWebState, + account: AccountResult | null, + session: SessionState | null, +): void { + const stored: StoredRegistrationState = { + version: 1, + startedAt: new Date().toISOString(), + mode, + step, + pdsHostname, + info: { ...info, password: undefined }, + externalDidWeb: { + keyMode: externalDidWeb.keyMode, + reservedSigningKey: externalDidWeb.reservedSigningKey, + byodPrivateKeyBase64: externalDidWeb.byodPrivateKey + ? uint8ArrayToBase64(externalDidWeb.byodPrivateKey) + : undefined, + byodPublicKeyMultibase: externalDidWeb.byodPublicKeyMultibase, + initialDidDocument: externalDidWeb.initialDidDocument, + updatedDidDocument: externalDidWeb.updatedDidDocument, + }, + account: account + ? { + did: account.did, + handle: account.handle, + setupToken: account.setupToken, + appPassword: account.appPassword, + appPasswordName: account.appPasswordName, + } + : null, + session: session + ? { + accessJwt: session.accessJwt, + refreshJwt: session.refreshJwt, + } + : null, + }; + + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); + } catch { /* localStorage unavailable */ } +} + +export function loadRegistrationState(): { + mode: RegistrationMode; + step: RegistrationStep; + pdsHostname: string; + info: RegistrationInfo; + externalDidWeb: ExternalDidWebState; + account: AccountResult | null; + session: SessionState | null; +} | null { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) return null; + + const state = JSON.parse(stored) as StoredRegistrationState; + + if (state.version !== 1) { + clearRegistrationState(); + return null; + } + + const startedAt = new Date(state.startedAt).getTime(); + if (Date.now() - startedAt > MAX_AGE_MS) { + clearRegistrationState(); + return null; + } + + return { + mode: state.mode, + step: state.step, + pdsHostname: state.pdsHostname, + info: state.info, + externalDidWeb: { + keyMode: state.externalDidWeb.keyMode, + reservedSigningKey: state.externalDidWeb.reservedSigningKey, + byodPrivateKey: state.externalDidWeb.byodPrivateKeyBase64 + ? base64ToUint8Array(state.externalDidWeb.byodPrivateKeyBase64) + : undefined, + byodPublicKeyMultibase: state.externalDidWeb.byodPublicKeyMultibase, + initialDidDocument: state.externalDidWeb.initialDidDocument, + updatedDidDocument: state.externalDidWeb.updatedDidDocument, + }, + account: state.account + ? { + did: state.account.did as AccountResult["did"], + handle: state.account.handle as AccountResult["handle"], + setupToken: state.account.setupToken, + appPassword: state.account.appPassword, + appPasswordName: state.account.appPasswordName, + } + : null, + session: state.session + ? { + accessJwt: state.session.accessJwt as SessionState["accessJwt"], + refreshJwt: state.session.refreshJwt as SessionState["refreshJwt"], + } + : null, + }; + } catch { + clearRegistrationState(); + return null; + } +} + +export function clearRegistrationState(): void { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { /* localStorage unavailable */ } +} + +export function hasPendingRegistration(): boolean { + const state = loadRegistrationState(); + return state !== null && state.step !== "info" && state.step !== "redirect-to-dashboard"; +} + +export function getRegistrationResumeInfo(): { + mode: RegistrationMode; + handle: string; + step: RegistrationStep; + did?: string; +} | null { + const state = loadRegistrationState(); + if (!state || state.step === "info" || state.step === "redirect-to-dashboard") { + return null; + } + + return { + mode: state.mode, + handle: state.info.handle, + step: state.step, + did: state.account?.did, + }; +} diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index a1e6267..15c6c92 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -703,7 +703,53 @@ "signingInAs": "Signing in as:", "permissionsRequested": "Permissions Requested", "required": "Required", - "rememberChoiceLabel": "Remember my choice for this application" + "rememberChoiceLabel": "Remember my choice for this application", + "scopes": { + "atproto": { + "name": "Full Account Access", + "description": "Full access to read, write, and manage this account" + }, + "atprotoWithGranular": { + "name": "AT Protocol Access", + "description": "AT Protocol baseline scope (permissions determined by selected options below)" + }, + "transitionGeneric": { + "name": "Transition Access", + "description": "Generic transition scope for compatibility" + }, + "transitionChat": { + "name": "Chat Access", + "description": "Access to Bluesky chat features" + }, + "transitionEmail": { + "name": "Email Access", + "description": "Read your account email address" + }, + "repoCreate": { + "name": "Create Records", + "description": "Create new records in your repository" + }, + "repoUpdate": { + "name": "Update Records", + "description": "Update existing records in your repository" + }, + "repoDelete": { + "name": "Delete Records", + "description": "Delete records from your repository" + }, + "blobAll": { + "name": "Upload Media", + "description": "Upload images, videos, and other media files" + }, + "repoFull": { + "name": "Full Repository Access", + "description": "Full read and write access to all repository records" + }, + "accountManage": { + "name": "Manage Account", + "description": "Manage account settings and preferences" + } + } }, "accounts": { "title": "Choose Account", @@ -999,6 +1045,12 @@ "accessLevel": "Access Level", "adding": "Adding...", "addControllerButton": "+ Add Controller", + "addControllerWarningTitle": "Important: This will change how you log in", + "addControllerWarningText": "Adding a controller means that only the controller account will be able to log into this account via OAuth. You will no longer be able to log in directly with your own credentials through third-party apps or the web interface.", + "addControllerWarningBullet1": "The controller will be able to act on your behalf with the permissions you grant", + "addControllerWarningBullet2": "You will need to log in as the controller first, then switch to this account", + "addControllerWarningBullet3": "You can remove the controller later to regain direct login access", + "addControllerConfirm": "I understand that I will no longer be able to log in directly", "controllerAdded": "Controller added successfully", "controllerRemoved": "Controller removed successfully", "failedToAddController": "Failed to add controller", diff --git a/frontend/src/locales/fi.json b/frontend/src/locales/fi.json index 2174bd4..00edfb6 100644 --- a/frontend/src/locales/fi.json +++ b/frontend/src/locales/fi.json @@ -703,7 +703,53 @@ "signingInAs": "Kirjaudutaan käyttäjänä:", "permissionsRequested": "Pyydetyt oikeudet", "required": "Vaaditaan", - "rememberChoiceLabel": "Muista valintani tälle sovellukselle" + "rememberChoiceLabel": "Muista valintani tälle sovellukselle", + "scopes": { + "atproto": { + "name": "Täysi käyttöoikeus", + "description": "Täysi oikeus lukea, kirjoittaa ja hallita tätä tiliä" + }, + "atprotoWithGranular": { + "name": "AT Protocol -käyttöoikeus", + "description": "AT Protocol -peruslaajuus (oikeudet määräytyvät alla valittujen vaihtoehtojen mukaan)" + }, + "transitionGeneric": { + "name": "Siirtymäkäyttöoikeus", + "description": "Yleinen siirtymälaajuus yhteensopivuutta varten" + }, + "transitionChat": { + "name": "Chat-käyttöoikeus", + "description": "Pääsy Bluesky-chat-ominaisuuksiin" + }, + "transitionEmail": { + "name": "Sähköpostikäyttöoikeus", + "description": "Lue tilisi sähköpostiosoite" + }, + "repoCreate": { + "name": "Luo tietueita", + "description": "Luo uusia tietueita tietovarastoosi" + }, + "repoUpdate": { + "name": "Päivitä tietueita", + "description": "Päivitä olemassa olevia tietueita tietovarastossasi" + }, + "repoDelete": { + "name": "Poista tietueita", + "description": "Poista tietueita tietovarastostasi" + }, + "blobAll": { + "name": "Lataa mediaa", + "description": "Lataa kuvia, videoita ja muita mediatiedostoja" + }, + "repoFull": { + "name": "Täysi tietovarastokäyttö", + "description": "Täysi luku- ja kirjoitusoikeus kaikkiin tietovaraston tietueisiin" + }, + "accountManage": { + "name": "Hallitse tiliä", + "description": "Hallitse tilin asetuksia ja asetuksia" + } + } }, "accounts": { "title": "Valitse tili", diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json index 86bf2ed..0d3adb6 100644 --- a/frontend/src/locales/ja.json +++ b/frontend/src/locales/ja.json @@ -696,7 +696,53 @@ "signingInAs": "サインイン中のアカウント:", "permissionsRequested": "リクエストされた権限", "required": "必須", - "rememberChoiceLabel": "このアプリに対する選択を記憶する" + "rememberChoiceLabel": "このアプリに対する選択を記憶する", + "scopes": { + "atproto": { + "name": "フルアクセス", + "description": "このアカウントの読み取り、書き込み、管理へのフルアクセス" + }, + "atprotoWithGranular": { + "name": "AT Protocol アクセス", + "description": "AT Protocol 基本スコープ(権限は以下で選択したオプションによって決まります)" + }, + "transitionGeneric": { + "name": "移行アクセス", + "description": "互換性のための汎用移行スコープ" + }, + "transitionChat": { + "name": "チャットアクセス", + "description": "Blueskyチャット機能へのアクセス" + }, + "transitionEmail": { + "name": "メールアクセス", + "description": "アカウントのメールアドレスを読み取る" + }, + "repoCreate": { + "name": "レコード作成", + "description": "リポジトリに新しいレコードを作成" + }, + "repoUpdate": { + "name": "レコード更新", + "description": "リポジトリの既存レコードを更新" + }, + "repoDelete": { + "name": "レコード削除", + "description": "リポジトリからレコードを削除" + }, + "blobAll": { + "name": "メディアアップロード", + "description": "画像、動画、その他のメディアファイルをアップロード" + }, + "repoFull": { + "name": "リポジトリフルアクセス", + "description": "すべてのリポジトリレコードへのフル読み書きアクセス" + }, + "accountManage": { + "name": "アカウント管理", + "description": "アカウント設定と設定を管理" + } + } }, "accounts": { "title": "アカウントを選択", diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index 95ae6b0..34ab0e9 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -696,7 +696,53 @@ "signingInAs": "로그인 계정:", "permissionsRequested": "요청된 권한", "required": "필수", - "rememberChoiceLabel": "이 앱에 대한 선택 기억하기" + "rememberChoiceLabel": "이 앱에 대한 선택 기억하기", + "scopes": { + "atproto": { + "name": "전체 액세스", + "description": "이 계정을 읽고, 쓰고, 관리하는 전체 액세스" + }, + "atprotoWithGranular": { + "name": "AT Protocol 액세스", + "description": "AT Protocol 기본 범위 (권한은 아래 선택한 옵션에 의해 결정됨)" + }, + "transitionGeneric": { + "name": "전환 액세스", + "description": "호환성을 위한 일반 전환 범위" + }, + "transitionChat": { + "name": "채팅 액세스", + "description": "Bluesky 채팅 기능 액세스" + }, + "transitionEmail": { + "name": "이메일 액세스", + "description": "계정 이메일 주소 읽기" + }, + "repoCreate": { + "name": "레코드 생성", + "description": "저장소에 새 레코드 생성" + }, + "repoUpdate": { + "name": "레코드 업데이트", + "description": "저장소의 기존 레코드 업데이트" + }, + "repoDelete": { + "name": "레코드 삭제", + "description": "저장소에서 레코드 삭제" + }, + "blobAll": { + "name": "미디어 업로드", + "description": "이미지, 비디오 및 기타 미디어 파일 업로드" + }, + "repoFull": { + "name": "전체 저장소 액세스", + "description": "모든 저장소 레코드에 대한 전체 읽기 및 쓰기 액세스" + }, + "accountManage": { + "name": "계정 관리", + "description": "계정 설정 및 환경설정 관리" + } + } }, "accounts": { "title": "계정 선택", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 1f810cc..c98263c 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -696,7 +696,53 @@ "signingInAs": "Loggar in som:", "permissionsRequested": "Begärda behörigheter", "required": "Krävs", - "rememberChoiceLabel": "Kom ihåg mitt val för denna applikation" + "rememberChoiceLabel": "Kom ihåg mitt val för denna applikation", + "scopes": { + "atproto": { + "name": "Full åtkomst", + "description": "Full åtkomst för att läsa, skriva och hantera detta konto" + }, + "atprotoWithGranular": { + "name": "AT Protocol-åtkomst", + "description": "AT Protocol basomfattning (behörigheter bestäms av valda alternativ nedan)" + }, + "transitionGeneric": { + "name": "Övergångsåtkomst", + "description": "Generisk övergångsomfattning för kompatibilitet" + }, + "transitionChat": { + "name": "Chattåtkomst", + "description": "Åtkomst till Bluesky-chattfunktioner" + }, + "transitionEmail": { + "name": "E-poståtkomst", + "description": "Läs din kontots e-postadress" + }, + "repoCreate": { + "name": "Skapa poster", + "description": "Skapa nya poster i ditt arkiv" + }, + "repoUpdate": { + "name": "Uppdatera poster", + "description": "Uppdatera befintliga poster i ditt arkiv" + }, + "repoDelete": { + "name": "Ta bort poster", + "description": "Ta bort poster från ditt arkiv" + }, + "blobAll": { + "name": "Ladda upp media", + "description": "Ladda upp bilder, videor och andra mediefiler" + }, + "repoFull": { + "name": "Full arkivåtkomst", + "description": "Full läs- och skrivåtkomst till alla arkivposter" + }, + "accountManage": { + "name": "Hantera konto", + "description": "Hantera kontoinställningar och preferenser" + } + } }, "accounts": { "title": "Välj konto", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 7079195..fbff9f5 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -696,7 +696,53 @@ "signingInAs": "登录账户:", "permissionsRequested": "请求的权限", "required": "必需", - "rememberChoiceLabel": "记住对此应用的授权选择" + "rememberChoiceLabel": "记住对此应用的授权选择", + "scopes": { + "atproto": { + "name": "完全访问", + "description": "完全访问权限以读取、写入和管理此账户" + }, + "atprotoWithGranular": { + "name": "AT Protocol 访问", + "description": "AT Protocol 基础范围(权限由下方选择的选项决定)" + }, + "transitionGeneric": { + "name": "过渡访问", + "description": "用于兼容性的通用过渡范围" + }, + "transitionChat": { + "name": "聊天访问", + "description": "访问 Bluesky 聊天功能" + }, + "transitionEmail": { + "name": "邮箱访问", + "description": "读取您的账户邮箱地址" + }, + "repoCreate": { + "name": "创建记录", + "description": "在您的仓库中创建新记录" + }, + "repoUpdate": { + "name": "更新记录", + "description": "更新您仓库中的现有记录" + }, + "repoDelete": { + "name": "删除记录", + "description": "从您的仓库中删除记录" + }, + "blobAll": { + "name": "上传媒体", + "description": "上传图片、视频和其他媒体文件" + }, + "repoFull": { + "name": "完全仓库访问", + "description": "对所有仓库记录的完全读写访问权限" + }, + "accountManage": { + "name": "管理账户", + "description": "管理账户设置和偏好" + } + } }, "accounts": { "title": "选择账户", diff --git a/frontend/src/routes/ActAs.svelte b/frontend/src/routes/ActAs.svelte index c6e61bd..f205614 100644 --- a/frontend/src/routes/ActAs.svelte +++ b/frontend/src/routes/ActAs.svelte @@ -1,7 +1,7 @@ @@ -262,7 +332,7 @@ {:else} - {#each Object.entries(scopeGroups) as [category, scopes]} + {#each scopeGroups as [category, scopes]}

{category}

{#each scopes as scope} @@ -274,8 +344,8 @@ onchange={() => handleScopeToggle(scope.scope)} />
- {scope.display_name} - {scope.description} + {getLocalizedScopeName(scope)} + {getLocalizedScopeDescription(scope)} {#if scope.required} {$_('oauth.consent.required')} {/if} @@ -419,10 +489,16 @@ .account-info .did { font-family: monospace; font-size: var(--text-sm); - color: var(--text-primary); + color: var(--text-secondary); word-break: break-all; } + .account-info .handle { + font-size: var(--text-base); + font-weight: var(--font-medium); + color: var(--text-primary); + } + .delegation-badge { display: inline-block; padding: var(--space-1) var(--space-2); diff --git a/frontend/src/routes/OAuthLogin.svelte b/frontend/src/routes/OAuthLogin.svelte index 2f954a1..a78331b 100644 --- a/frontend/src/routes/OAuthLogin.svelte +++ b/frontend/src/routes/OAuthLogin.svelte @@ -1,6 +1,7 @@