From 4fcb889942459e4288857ca986ba46366fcd7cf2 Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 27 Mar 2026 21:24:26 -0700 Subject: [PATCH] feat(tranquil-store): repository traits on MetastoreClient Lewis: May this revision serve well! --- Cargo.lock | 22 + .../src/server/trusted_devices.rs | 12 +- crates/tranquil-db-traits/src/oauth.rs | 5 +- crates/tranquil-db/src/postgres/oauth.rs | 5 +- .../src/endpoints/authorize/login.rs | 12 +- .../src/endpoints/authorize/passkey.rs | 9 +- .../src/endpoints/authorize/two_factor.rs | 3 +- crates/tranquil-pds/src/repo/mod.rs | 73 +- crates/tranquil-pds/src/scheduled.rs | 43 +- crates/tranquil-pds/src/state.rs | 60 +- crates/tranquil-repo/src/lib.rs | 10 +- crates/tranquil-store/Cargo.toml | 1 + crates/tranquil-store/src/metastore/client.rs | 3885 ++++++++++++++- .../src/metastore/commit_ops.rs | 2 +- .../src/metastore/delegation_ops.rs | 412 ++ .../src/metastore/delegations.rs | 201 + .../tranquil-store/src/metastore/handler.rs | 4240 ++++++++++++++++- .../tranquil-store/src/metastore/infra_ops.rs | 1209 +++++ .../src/metastore/infra_schema.rs | 730 +++ crates/tranquil-store/src/metastore/keys.rs | 133 + crates/tranquil-store/src/metastore/mod.rs | 69 + .../tranquil-store/src/metastore/oauth_ops.rs | 1602 +++++++ .../src/metastore/oauth_schema.rs | 556 +++ .../src/metastore/session_ops.rs | 919 ++++ .../tranquil-store/src/metastore/sessions.rs | 459 ++ .../tranquil-store/src/metastore/sso_ops.rs | 482 ++ .../src/metastore/sso_schema.rs | 227 + .../tranquil-store/src/metastore/user_ops.rs | 3099 ++++++++++++ crates/tranquil-store/src/metastore/users.rs | 829 ++++ 29 files changed, 19220 insertions(+), 89 deletions(-) create mode 100644 crates/tranquil-store/src/metastore/delegation_ops.rs create mode 100644 crates/tranquil-store/src/metastore/delegations.rs create mode 100644 crates/tranquil-store/src/metastore/infra_ops.rs create mode 100644 crates/tranquil-store/src/metastore/infra_schema.rs create mode 100644 crates/tranquil-store/src/metastore/oauth_ops.rs create mode 100644 crates/tranquil-store/src/metastore/oauth_schema.rs create mode 100644 crates/tranquil-store/src/metastore/session_ops.rs create mode 100644 crates/tranquil-store/src/metastore/sessions.rs create mode 100644 crates/tranquil-store/src/metastore/sso_ops.rs create mode 100644 crates/tranquil-store/src/metastore/sso_schema.rs create mode 100644 crates/tranquil-store/src/metastore/user_ops.rs create mode 100644 crates/tranquil-store/src/metastore/users.rs diff --git a/Cargo.lock b/Cargo.lock index 5c2b887..532d671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6979,6 +6979,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tikv-jemalloc-sys" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + [[package]] name = "time" version = "0.3.47" @@ -7863,10 +7883,12 @@ dependencies = [ "sqlx", "tempfile", "thiserror 2.0.18", + "tikv-jemallocator", "tokio", "tracing", "tranquil-db", "tranquil-db-traits", + "tranquil-oauth", "tranquil-repo", "tranquil-types", "uuid", diff --git a/crates/tranquil-api/src/server/trusted_devices.rs b/crates/tranquil-api/src/server/trusted_devices.rs index 96b9894..d12c5f1 100644 --- a/crates/tranquil-api/src/server/trusted_devices.rs +++ b/crates/tranquil-api/src/server/trusted_devices.rs @@ -127,7 +127,7 @@ pub async fn revoke_trusted_device( state .repos .oauth - .revoke_device_trust(&input.device_id) + .revoke_device_trust(&input.device_id, &auth.did) .await .log_db_err("revoking device trust")?; @@ -166,7 +166,7 @@ pub async fn update_trusted_device( state .repos .oauth - .update_device_friendly_name(&input.device_id, input.friendly_name.as_deref()) + .update_device_friendly_name(&input.device_id, &auth.did, input.friendly_name.as_deref()) .await .log_db_err("updating device friendly name")?; @@ -198,18 +198,22 @@ pub async fn is_device_trusted( pub async fn trust_device( oauth_repo: &dyn OAuthRepository, device_id: &DeviceId, + did: &tranquil_types::Did, ) -> Result<(), tranquil_db_traits::DbError> { let now = Utc::now(); let trusted_until = now + Duration::days(TRUST_DURATION_DAYS); - oauth_repo.trust_device(device_id, now, trusted_until).await + oauth_repo + .trust_device(device_id, did, now, trusted_until) + .await } pub async fn extend_device_trust( oauth_repo: &dyn OAuthRepository, device_id: &DeviceId, + did: &tranquil_types::Did, ) -> Result<(), tranquil_db_traits::DbError> { let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS); oauth_repo - .extend_device_trust(device_id, trusted_until) + .extend_device_trust(device_id, did, trusted_until) .await } diff --git a/crates/tranquil-db-traits/src/oauth.rs b/crates/tranquil-db-traits/src/oauth.rs index 193de83..acff060 100644 --- a/crates/tranquil-db-traits/src/oauth.rs +++ b/crates/tranquil-db-traits/src/oauth.rs @@ -291,21 +291,24 @@ pub trait OAuthRepository: Send + Sync { device_id: &DeviceId, did: &Did, ) -> Result; - async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError>; + async fn revoke_device_trust(&self, device_id: &DeviceId, did: &Did) -> Result<(), DbError>; async fn update_device_friendly_name( &self, device_id: &DeviceId, + did: &Did, friendly_name: Option<&str>, ) -> Result<(), DbError>; async fn trust_device( &self, device_id: &DeviceId, + did: &Did, trusted_at: DateTime, trusted_until: DateTime, ) -> Result<(), DbError>; async fn extend_device_trust( &self, device_id: &DeviceId, + did: &Did, trusted_until: DateTime, ) -> Result<(), DbError>; diff --git a/crates/tranquil-db/src/postgres/oauth.rs b/crates/tranquil-db/src/postgres/oauth.rs index 360e2c0..67adddb 100644 --- a/crates/tranquil-db/src/postgres/oauth.rs +++ b/crates/tranquil-db/src/postgres/oauth.rs @@ -1194,7 +1194,7 @@ impl OAuthRepository for PostgresOAuthRepository { Ok(exists.is_some()) } - async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError> { + async fn revoke_device_trust(&self, device_id: &DeviceId, _did: &Did) -> Result<(), DbError> { sqlx::query!( "UPDATE oauth_device SET trusted_at = NULL, trusted_until = NULL WHERE id = $1", device_id.as_str() @@ -1208,6 +1208,7 @@ impl OAuthRepository for PostgresOAuthRepository { async fn update_device_friendly_name( &self, device_id: &DeviceId, + _did: &Did, friendly_name: Option<&str>, ) -> Result<(), DbError> { sqlx::query!( @@ -1224,6 +1225,7 @@ impl OAuthRepository for PostgresOAuthRepository { async fn trust_device( &self, device_id: &DeviceId, + _did: &Did, trusted_at: DateTime, trusted_until: DateTime, ) -> Result<(), DbError> { @@ -1242,6 +1244,7 @@ impl OAuthRepository for PostgresOAuthRepository { async fn extend_device_trust( &self, device_id: &DeviceId, + _did: &Did, trusted_until: DateTime, ) -> Result<(), DbError> { sqlx::query!( diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs index 2360af5..e07f74b 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs @@ -529,9 +529,12 @@ pub async fn authorize_post( if device_is_trusted { if let Some(ref dev_id) = device_cookie { - let _ = - tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), dev_id) - .await; + let _ = tranquil_api::server::extend_device_trust( + state.repos.oauth.as_ref(), + dev_id, + &user.did, + ) + .await; } } else { if state @@ -892,7 +895,8 @@ pub async fn authorize_select( .into_response(); } let _ = - tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), &device_id).await; + tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), &device_id, &did) + .await; } if user.two_factor_enabled { let _ = state diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs index 59ed122..aa753d2 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs @@ -1074,9 +1074,12 @@ pub async fn authorize_passkey_finish( if device_is_trusted { if let Some(ref dev_id) = device_cookie { - let _ = - tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), dev_id) - .await; + let _ = tranquil_api::server::extend_device_trust( + state.repos.oauth.as_ref(), + dev_id, + &did, + ) + .await; } } else { let user = match state.repos.user.get_2fa_status_by_did(&did).await { diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs index 48fdf1f..660af6f 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs @@ -273,7 +273,8 @@ pub async fn authorize_2fa_post( .upsert_account_device(&did, &trust_device_id) .await; let _ = - tranquil_api::server::trust_device(state.repos.oauth.as_ref(), &trust_device_id).await; + tranquil_api::server::trust_device(state.repos.oauth.as_ref(), &trust_device_id, &did) + .await; } let requested_scope_str = request_data .parameters diff --git a/crates/tranquil-pds/src/repo/mod.rs b/crates/tranquil-pds/src/repo/mod.rs index 7a97b4f..761d87f 100644 --- a/crates/tranquil-pds/src/repo/mod.rs +++ b/crates/tranquil-pds/src/repo/mod.rs @@ -1 +1,72 @@ -pub use tranquil_repo::{PostgresBlockStore, TrackingBlockStore}; +pub use tranquil_repo::PostgresBlockStore; + +pub type TrackingBlockStore = tranquil_repo::TrackingBlockStore; + +use bytes::Bytes; +use cid::Cid; +use jacquard_repo::error::RepoError; +use jacquard_repo::repo::CommitData; +use jacquard_repo::storage::BlockStore; +use tranquil_store::blockstore::TranquilBlockStore; + +#[derive(Clone)] +pub enum AnyBlockStore { + Postgres(PostgresBlockStore), + TranquilStore(TranquilBlockStore), +} + +impl AnyBlockStore { + pub fn as_postgres(&self) -> Option<&PostgresBlockStore> { + match self { + Self::Postgres(s) => Some(s), + Self::TranquilStore(_) => None, + } + } +} + +impl BlockStore for AnyBlockStore { + async fn get(&self, cid: &Cid) -> Result, RepoError> { + match self { + Self::Postgres(s) => s.get(cid).await, + Self::TranquilStore(s) => s.get(cid).await, + } + } + + async fn put(&self, data: &[u8]) -> Result { + match self { + Self::Postgres(s) => s.put(data).await, + Self::TranquilStore(s) => s.put(data).await, + } + } + + async fn has(&self, cid: &Cid) -> Result { + match self { + Self::Postgres(s) => s.has(cid).await, + Self::TranquilStore(s) => s.has(cid).await, + } + } + + async fn put_many( + &self, + blocks: impl IntoIterator + Send, + ) -> Result<(), RepoError> { + match self { + Self::Postgres(s) => s.put_many(blocks).await, + Self::TranquilStore(s) => s.put_many(blocks).await, + } + } + + async fn get_many(&self, cids: &[Cid]) -> Result>, RepoError> { + match self { + Self::Postgres(s) => s.get_many(cids).await, + Self::TranquilStore(s) => s.get_many(cids).await, + } + } + + async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> { + match self { + Self::Postgres(s) => s.apply_commit(commit).await, + Self::TranquilStore(s) => s.apply_commit(commit).await, + } + } +} diff --git a/crates/tranquil-pds/src/scheduled.rs b/crates/tranquil-pds/src/scheduled.rs index 0626ea7..209d069 100644 --- a/crates/tranquil-pds/src/scheduled.rs +++ b/crates/tranquil-pds/src/scheduled.rs @@ -15,7 +15,7 @@ use tranquil_db_traits::{ }; use tranquil_types::{AtUri, CidLink, Did}; -use crate::repo::PostgresBlockStore; +use crate::repo::AnyBlockStore; use crate::storage::BlobStorage; use crate::sync::car::encode_car_header; @@ -44,7 +44,7 @@ impl std::fmt::Display for GenesisBackfillError { async fn process_genesis_commit( repo_repo: &dyn RepoRepository, - block_store: &PostgresBlockStore, + block_store: &AnyBlockStore, row: BrokenGenesisCommit, ) -> Result<(Did, SequenceNumber), (SequenceNumber, GenesisBackfillError)> { let commit_cid_str = row @@ -69,7 +69,7 @@ async fn process_genesis_commit( pub async fn backfill_genesis_commit_blocks( repo_repo: Arc, - block_store: PostgresBlockStore, + block_store: AnyBlockStore, ) { let broken_genesis_commits = match repo_repo.get_broken_genesis_commits().await { Ok(rows) => rows, @@ -122,7 +122,7 @@ pub async fn backfill_genesis_commit_blocks( async fn process_repo_rev( repo_repo: &dyn RepoRepository, - block_store: &PostgresBlockStore, + block_store: &AnyBlockStore, user_id: uuid::Uuid, repo_root_cid: String, ) -> Result { @@ -147,10 +147,7 @@ async fn process_repo_rev( Ok(user_id) } -pub async fn backfill_repo_rev( - repo_repo: Arc, - block_store: PostgresBlockStore, -) { +pub async fn backfill_repo_rev(repo_repo: Arc, block_store: AnyBlockStore) { let repos_missing_rev = match repo_repo.get_repos_without_rev().await { Ok(rows) => rows, Err(e) => { @@ -197,7 +194,7 @@ pub async fn backfill_repo_rev( async fn process_user_blocks( repo_repo: &dyn RepoRepository, - block_store: &PostgresBlockStore, + block_store: &AnyBlockStore, user_id: uuid::Uuid, repo_root_cid: String, repo_rev: Option, @@ -218,10 +215,7 @@ async fn process_user_blocks( Ok((user_id, count)) } -pub async fn backfill_user_blocks( - repo_repo: Arc, - block_store: PostgresBlockStore, -) { +pub async fn backfill_user_blocks(repo_repo: Arc, block_store: AnyBlockStore) { let users_without_blocks = match repo_repo.get_users_without_blocks().await { Ok(rows) => rows, Err(e) => { @@ -271,7 +265,7 @@ pub async fn backfill_user_blocks( } pub async fn collect_current_repo_blocks( - block_store: &PostgresBlockStore, + block_store: &AnyBlockStore, head_cid: &Cid, ) -> anyhow::Result>> { let mut block_cids: Vec> = Vec::new(); @@ -324,7 +318,7 @@ pub async fn collect_current_repo_blocks( async fn process_record_blobs( repo_repo: &dyn RepoRepository, - block_store: &PostgresBlockStore, + block_store: &AnyBlockStore, user_id: uuid::Uuid, did: Did, ) -> Result<(uuid::Uuid, Did, usize), (uuid::Uuid, &'static str)> { @@ -383,10 +377,7 @@ async fn process_record_blobs( Ok((user_id, did, blob_refs_found)) } -pub async fn backfill_record_blobs( - repo_repo: Arc, - block_store: PostgresBlockStore, -) { +pub async fn backfill_record_blobs(repo_repo: Arc, block_store: AnyBlockStore) { let users_needing_backfill = match repo_repo.get_users_needing_record_blobs_backfill(100).await { Ok(rows) => rows, @@ -437,7 +428,7 @@ pub async fn start_scheduled_tasks( blob_store: Arc, sso_repo: Arc, repo_repo: Arc, - block_store: PostgresBlockStore, + block_store: AnyBlockStore, shutdown: CancellationToken, ) { let cfg = tranquil_config::get(); @@ -502,7 +493,9 @@ pub async fn start_scheduled_tasks( } } _ = gc_ticker.tick() => { - if let Err(e) = run_block_gc(repo_repo.as_ref(), &block_store).await { + if let Some(pg) = block_store.as_postgres() + && let Err(e) = run_block_gc(repo_repo.as_ref(), pg).await + { error!("Block GC error: {e}"); } } @@ -514,7 +507,7 @@ const BLOCK_GC_BATCH_SIZE: i64 = 1000; async fn run_block_gc( repo_repo: &dyn RepoRepository, - block_store: &PostgresBlockStore, + block_store: &crate::repo::PostgresBlockStore, ) -> anyhow::Result<()> { let mut total_deleted: u64 = 0; @@ -632,11 +625,9 @@ async fn delete_account_data( } pub async fn generate_repo_car( - block_store: &PostgresBlockStore, + block_store: &AnyBlockStore, head_cid: &Cid, ) -> anyhow::Result> { - use jacquard_repo::storage::BlockStore; - let block_cids_bytes = collect_current_repo_blocks(block_store, head_cid).await?; let block_cids: Vec = block_cids_bytes .iter() @@ -686,7 +677,7 @@ fn encode_car_block(cid: &Cid, block: &[u8]) -> Vec { pub async fn generate_repo_car_from_user_blocks( repo_repo: &dyn tranquil_db_traits::RepoRepository, - block_store: &PostgresBlockStore, + block_store: &AnyBlockStore, user_id: uuid::Uuid, _head_cid: &Cid, ) -> anyhow::Result> { diff --git a/crates/tranquil-pds/src/state.rs b/crates/tranquil-pds/src/state.rs index fe05333..bfd3fc9 100644 --- a/crates/tranquil-pds/src/state.rs +++ b/crates/tranquil-pds/src/state.rs @@ -33,7 +33,7 @@ pub fn init_rate_limit_override() { #[derive(Clone)] pub struct AppState { pub repos: Arc, - pub block_store: PostgresBlockStore, + pub block_store: crate::repo::AnyBlockStore, pub blob_store: Arc, pub firehose_tx: broadcast::Sender, pub rate_limiters: Arc, @@ -266,12 +266,16 @@ impl AppState { let mut repos = PostgresRepositories::new(db.clone()); let cfg = tranquil_config::get(); - if cfg.storage.repo_backend() == tranquil_config::RepoBackend::TranquilStore { - wire_tranquil_store(&mut repos, &cfg.tranquil_store); - } + let block_store = + match cfg.storage.repo_backend() == tranquil_config::RepoBackend::TranquilStore { + true => { + let bs = wire_tranquil_store(&mut repos, &cfg.tranquil_store, shutdown.clone()); + crate::repo::AnyBlockStore::TranquilStore(bs) + } + false => crate::repo::AnyBlockStore::Postgres(PostgresBlockStore::new(db)), + }; let repos = Arc::new(repos); - let block_store = PostgresBlockStore::new(db); let blob_store = create_blob_storage().await; let firehose_buffer_size = tranquil_config::get().firehose.buffer_size; @@ -389,8 +393,10 @@ impl AppState { fn wire_tranquil_store( repos: &mut PostgresRepositories, store_cfg: &tranquil_config::TranquilStoreConfig, -) { + shutdown: CancellationToken, +) -> tranquil_store::blockstore::TranquilBlockStore { use tranquil_store::RealIO; + use tranquil_store::blockstore::{BlockStoreConfig, TranquilBlockStore}; use tranquil_store::eventlog::{EventLog, EventLogBridge, EventLogConfig}; use tranquil_store::metastore::client::MetastoreClient; use tranquil_store::metastore::handler::HandlerPool; @@ -404,9 +410,15 @@ fn wire_tranquil_store( }; let metastore_dir = data_dir.join("metastore"); let segments_dir = data_dir.join("eventlog").join("segments"); + let blockstore_data_dir = data_dir.join("blockstore").join("data"); + let blockstore_index_dir = data_dir.join("blockstore").join("index"); std::fs::create_dir_all(&metastore_dir).expect("failed to create metastore directory"); std::fs::create_dir_all(&segments_dir).expect("failed to create eventlog segments directory"); + std::fs::create_dir_all(&blockstore_data_dir) + .expect("failed to create blockstore data directory"); + std::fs::create_dir_all(&blockstore_index_dir) + .expect("failed to create blockstore index directory"); let metastore_config = store_cfg .memory_budget_mb @@ -418,6 +430,14 @@ fn wire_tranquil_store( let metastore = Metastore::open(&metastore_dir, metastore_config).expect("failed to open metastore"); + let blockstore = TranquilBlockStore::open(BlockStoreConfig { + data_dir: blockstore_data_dir, + index_dir: blockstore_index_dir, + max_file_size: tranquil_store::blockstore::DEFAULT_MAX_FILE_SIZE, + group_commit: Default::default(), + }) + .expect("failed to open blockstore"); + let event_log = EventLog::open( EventLogConfig { segments_dir, @@ -441,13 +461,35 @@ fn wire_tranquil_store( let notifier = bridge.notifier(); - let pool = HandlerPool::spawn::(metastore, bridge, None, store_cfg.handler_threads); + let pool = Arc::new(HandlerPool::spawn::( + metastore, + bridge, + Some(blockstore.clone()), + store_cfg.handler_threads, + )); - let client = MetastoreClient::::new(Arc::new(pool)); + tokio::spawn({ + let pool = Arc::clone(&pool); + async move { + shutdown.cancelled().await; + pool.close().await; + } + }); + + let client = MetastoreClient::::new(pool); tracing::info!(data_dir = %store_cfg.data_dir, "tranquil-store data directory"); repos.repo = Arc::new(client.clone()); - repos.backlink = Arc::new(client); + repos.backlink = Arc::new(client.clone()); + repos.blob = Arc::new(client.clone()); + repos.user = Arc::new(client.clone()); + repos.session = Arc::new(client.clone()); + repos.oauth = Arc::new(client.clone()); + repos.infra = Arc::new(client.clone()); + repos.delegation = Arc::new(client.clone()); + repos.sso = Arc::new(client); repos.event_notifier = Arc::new(notifier); + + blockstore } diff --git a/crates/tranquil-repo/src/lib.rs b/crates/tranquil-repo/src/lib.rs index 98c5037..c256c4b 100644 --- a/crates/tranquil-repo/src/lib.rs +++ b/crates/tranquil-repo/src/lib.rs @@ -150,14 +150,14 @@ impl BlockStore for PostgresBlockStore { } #[derive(Clone)] -pub struct TrackingBlockStore { - inner: PostgresBlockStore, +pub struct TrackingBlockStore { + inner: S, written_cids: Arc>>, read_cids: Arc>>, } -impl TrackingBlockStore { - pub fn new(store: PostgresBlockStore) -> Self { +impl TrackingBlockStore { + pub fn new(store: S) -> Self { Self { inner: store, written_cids: Arc::new(Mutex::new(Vec::new())), @@ -188,7 +188,7 @@ impl TrackingBlockStore { } } -impl BlockStore for TrackingBlockStore { +impl BlockStore for TrackingBlockStore { async fn get(&self, cid: &Cid) -> Result, RepoError> { let result = self.inner.get(cid).await?; if result.is_some() { diff --git a/crates/tranquil-store/Cargo.toml b/crates/tranquil-store/Cargo.toml index 8b50fd3..3432306 100644 --- a/crates/tranquil-store/Cargo.toml +++ b/crates/tranquil-store/Cargo.toml @@ -22,6 +22,7 @@ chrono = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tranquil-db-traits = { workspace = true } +tranquil-oauth = { workspace = true } tranquil-types = { workspace = true } jacquard-repo = { workspace = true } cid = { workspace = true } diff --git a/crates/tranquil-store/src/metastore/client.rs b/crates/tranquil-store/src/metastore/client.rs index c398a54..0367b75 100644 --- a/crates/tranquil-store/src/metastore/client.rs +++ b/crates/tranquil-store/src/metastore/client.rs @@ -5,17 +5,38 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use tokio::sync::oneshot; use tranquil_db_traits::{ - AccountStatus, ApplyCommitError, ApplyCommitInput, ApplyCommitResult, Backlink, - BrokenGenesisCommit, CommitEventData, DbError, EventBlocksCids, ImportBlock, ImportRecord, - ImportRepoError, RepoAccountInfo, RepoInfo, RepoListItem, RepoWithoutRev, SequenceNumber, - SequencedEvent, UserNeedingRecordBlobsBackfill, UserWithoutBlocks, + AccountSearchResult, AccountStatus, AdminAccountInfo, ApplyCommitError, ApplyCommitInput, + ApplyCommitResult, Backlink, BrokenGenesisCommit, CommitEventData, CommsChannel, CommsType, + CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput, + CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult, + CreateSsoAccountInput, DbError, DeletionRequest, DidWebOverrides, EventBlocksCids, ImportBlock, + ImportRecord, ImportRepoError, InviteCodeError, InviteCodeInfo, InviteCodeRow, + InviteCodeSortOrder, InviteCodeUse, MigrationReactivationError, MigrationReactivationInput, + NotificationHistoryRow, NotificationPrefs, OAuthTokenWithUser, PasswordResetResult, + QueuedComms, ReactivatedAccountInfo, RecoverPasskeyAccountInput, RecoverPasskeyAccountResult, + RepoAccountInfo, RepoInfo, RepoListItem, RepoWithoutRev, ReservedSigningKey, + ScheduledDeletionAccount, ScopePreference, SequenceNumber, SequencedEvent, StoredBackupCode, + StoredPasskey, TokenFamilyId, TotpRecord, TotpRecordState, User2faStatus, UserAuthInfo, + UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, UserForDeletion, + UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, UserForPasskeySetup, + UserForRecovery, UserForVerification, UserIdAndHandle, UserIdAndPasswordHash, + UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId, UserLegacyLoginPref, + UserLoginCheck, UserLoginFull, UserLoginInfo, UserNeedingRecordBlobsBackfill, UserPasswordInfo, + UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo, UserStatus, + UserVerificationInfo, UserWithKey, UserWithoutBlocks, ValidatedInviteCode, + WebauthnChallengeType, +}; +use tranquil_oauth::{AuthorizedClientData, DeviceData, RequestData, TokenData}; +use tranquil_types::{ + AtUri, AuthorizationCode, CidLink, ClientId, DPoPProofId, DeviceId, Did, Handle, Nsid, + RefreshToken, RequestId, Rkey, TokenId, }; -use tranquil_types::{AtUri, CidLink, Did, Handle, Nsid, Rkey}; use uuid::Uuid; use super::handler::{ - BacklinkRequest, BlobRequest, CommitRequest, EventRequest, HandlerPool, MetastoreRequest, - RecordRequest, RepoRequest, UserBlockRequest, + BacklinkRequest, BlobRequest, CommitRequest, DelegationRequest, EventRequest, HandlerPool, + InfraRequest, MetastoreRequest, OAuthRequest, RecordRequest, RepoRequest, SessionRequest, + SsoRequest, UserBlockRequest, UserRequest, }; use super::keys::UserHash; use crate::io::StorageIO; @@ -39,6 +60,31 @@ async fn recv_import( .map_err(|_| ImportRepoError::Database("metastore handler thread closed".to_string()))? } +async fn recv_invite( + rx: oneshot::Receiver>, +) -> Result<(), InviteCodeError> { + rx.await.map_err(|_| { + InviteCodeError::DatabaseError(DbError::Connection( + "metastore handler thread closed".to_string(), + )) + })? +} + +async fn recv_create_account( + rx: oneshot::Receiver>, +) -> Result { + rx.await + .map_err(|_| CreateAccountError::Database("metastore handler thread closed".to_string()))? +} + +async fn recv_migration_reactivation( + rx: oneshot::Receiver>, +) -> Result { + rx.await.map_err(|_| { + MigrationReactivationError::Database("metastore handler thread closed".to_string()) + })? +} + pub struct MetastoreClient { pool: Arc, _phantom: PhantomData, @@ -652,7 +698,7 @@ impl tranquil_db_traits::RepoRepository for MetastoreCli self.pool .send(MetastoreRequest::Repo(RepoRequest::ListReposPaginated { cursor_user_hash: cursor_hash, - limit: usize::try_from(limit).unwrap_or(usize::MAX), + limit: usize::try_from(limit).unwrap_or(0), tx, }))?; recv(rx).await @@ -1021,3 +1067,3826 @@ impl tranquil_db_traits::BlobRepository for MetastoreCli recv(rx).await } } + +#[async_trait] +impl tranquil_db_traits::DelegationRepository for MetastoreClient { + async fn is_delegated_account(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::IsDelegatedAccount { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn create_delegation( + &self, + delegated_did: &Did, + controller_did: &Did, + granted_scopes: &tranquil_db_traits::DbScope, + granted_by: &Did, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::CreateDelegation { + delegated_did: delegated_did.clone(), + controller_did: controller_did.clone(), + granted_scopes: granted_scopes.clone(), + granted_by: granted_by.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn revoke_delegation( + &self, + delegated_did: &Did, + controller_did: &Did, + revoked_by: &Did, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::RevokeDelegation { + delegated_did: delegated_did.clone(), + controller_did: controller_did.clone(), + revoked_by: revoked_by.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn update_delegation_scopes( + &self, + delegated_did: &Did, + controller_did: &Did, + new_scopes: &tranquil_db_traits::DbScope, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::UpdateDelegationScopes { + delegated_did: delegated_did.clone(), + controller_did: controller_did.clone(), + new_scopes: new_scopes.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_delegation( + &self, + delegated_did: &Did, + controller_did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::GetDelegation { + delegated_did: delegated_did.clone(), + controller_did: controller_did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_delegations_for_account( + &self, + delegated_did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::GetDelegationsForAccount { + delegated_did: delegated_did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_accounts_controlled_by( + &self, + controller_did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::GetAccountsControlledBy { + controller_did: controller_did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn count_active_controllers(&self, delegated_did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::CountActiveControllers { + delegated_did: delegated_did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn controls_any_accounts(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::ControlsAnyAccounts { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn log_delegation_action( + &self, + delegated_did: &Did, + actor_did: &Did, + controller_did: Option<&Did>, + action_type: tranquil_db_traits::DelegationActionType, + action_details: Option, + ip_address: Option<&str>, + user_agent: Option<&str>, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::LogDelegationAction { + delegated_did: delegated_did.clone(), + actor_did: actor_did.clone(), + controller_did: controller_did.cloned(), + action_type, + action_details, + ip_address: ip_address.map(str::to_owned), + user_agent: user_agent.map(str::to_owned), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_audit_log_for_account( + &self, + delegated_did: &Did, + limit: i64, + offset: i64, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::GetAuditLogForAccount { + delegated_did: delegated_did.clone(), + limit, + offset, + tx, + }, + ))?; + recv(rx).await + } + + async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Delegation( + DelegationRequest::CountAuditLogEntries { + delegated_did: delegated_did.clone(), + tx, + }, + ))?; + recv(rx).await + } +} + +#[async_trait] +impl tranquil_db_traits::SsoRepository for MetastoreClient { + async fn create_external_identity( + &self, + did: &Did, + provider: tranquil_db_traits::SsoProviderType, + provider_user_id: &str, + provider_username: Option<&str>, + provider_email: Option<&str>, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Sso(SsoRequest::CreateExternalIdentity { + did: did.clone(), + provider, + provider_user_id: provider_user_id.to_owned(), + provider_username: provider_username.map(str::to_owned), + provider_email: provider_email.map(str::to_owned), + tx, + }))?; + recv(rx).await + } + + async fn get_external_identity_by_provider( + &self, + provider: tranquil_db_traits::SsoProviderType, + provider_user_id: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Sso( + SsoRequest::GetExternalIdentityByProvider { + provider, + provider_user_id: provider_user_id.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_external_identities_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Sso( + SsoRequest::GetExternalIdentitiesByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn update_external_identity_login( + &self, + id: Uuid, + provider_username: Option<&str>, + provider_email: Option<&str>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Sso( + SsoRequest::UpdateExternalIdentityLogin { + id, + provider_username: provider_username.map(str::to_owned), + provider_email: provider_email.map(str::to_owned), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_external_identity(&self, id: Uuid, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Sso(SsoRequest::DeleteExternalIdentity { + id, + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn create_sso_auth_state( + &self, + state: &str, + request_uri: &str, + provider: tranquil_db_traits::SsoProviderType, + action: tranquil_db_traits::SsoAction, + nonce: Option<&str>, + code_verifier: Option<&str>, + did: Option<&Did>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Sso(SsoRequest::CreateSsoAuthState { + state: state.to_owned(), + request_uri: request_uri.to_owned(), + provider, + action, + nonce: nonce.map(str::to_owned), + code_verifier: code_verifier.map(str::to_owned), + did: did.cloned(), + tx, + }))?; + recv(rx).await + } + + async fn consume_sso_auth_state( + &self, + state: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Sso(SsoRequest::ConsumeSsoAuthState { + state: state.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn cleanup_expired_sso_auth_states(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Sso( + SsoRequest::CleanupExpiredSsoAuthStates { tx }, + ))?; + recv(rx).await + } + + async fn create_pending_registration( + &self, + token: &str, + request_uri: &str, + provider: tranquil_db_traits::SsoProviderType, + provider_user_id: &str, + provider_username: Option<&str>, + provider_email: Option<&str>, + provider_email_verified: bool, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Sso( + SsoRequest::CreatePendingRegistration { + token: token.to_owned(), + request_uri: request_uri.to_owned(), + provider, + provider_user_id: provider_user_id.to_owned(), + provider_username: provider_username.map(str::to_owned), + provider_email: provider_email.map(str::to_owned), + provider_email_verified, + tx, + }, + ))?; + recv(rx).await + } + + async fn get_pending_registration( + &self, + token: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Sso(SsoRequest::GetPendingRegistration { + token: token.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn consume_pending_registration( + &self, + token: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Sso( + SsoRequest::ConsumePendingRegistration { + token: token.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn cleanup_expired_pending_registrations(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Sso( + SsoRequest::CleanupExpiredPendingRegistrations { tx }, + ))?; + recv(rx).await + } +} + +#[async_trait] +impl tranquil_db_traits::SessionRepository for MetastoreClient { + async fn create_session( + &self, + data: &tranquil_db_traits::SessionTokenCreate, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Session(SessionRequest::CreateSession { + data: data.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_session_by_access_jti( + &self, + access_jti: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetSessionByAccessJti { + access_jti: access_jti.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_session_for_refresh( + &self, + refresh_jti: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetSessionForRefresh { + refresh_jti: refresh_jti.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn update_session_tokens( + &self, + session_id: tranquil_db_traits::SessionId, + new_access_jti: &str, + new_refresh_jti: &str, + new_access_expires_at: DateTime, + new_refresh_expires_at: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::UpdateSessionTokens { + session_id, + new_access_jti: new_access_jti.to_owned(), + new_refresh_jti: new_refresh_jti.to_owned(), + new_access_expires_at, + new_refresh_expires_at, + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_session_by_access_jti(&self, access_jti: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::DeleteSessionByAccessJti { + access_jti: access_jti.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_session_by_id( + &self, + session_id: tranquil_db_traits::SessionId, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::DeleteSessionById { session_id, tx }, + ))?; + recv(rx).await + } + + async fn delete_sessions_by_did(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::DeleteSessionsByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_sessions_by_did_except_jti( + &self, + did: &Did, + except_jti: &str, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::DeleteSessionsByDidExceptJti { + did: did.clone(), + except_jti: except_jti.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn list_sessions_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::ListSessionsByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_session_access_jti_by_id( + &self, + session_id: tranquil_db_traits::SessionId, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetSessionAccessJtiById { + session_id, + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_sessions_by_app_password( + &self, + did: &Did, + app_password_name: &str, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::DeleteSessionsByAppPassword { + did: did.clone(), + app_password_name: app_password_name.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_session_jtis_by_app_password( + &self, + did: &Did, + app_password_name: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetSessionJtisByAppPassword { + did: did.clone(), + app_password_name: app_password_name.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn check_refresh_token_used( + &self, + refresh_jti: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::CheckRefreshTokenUsed { + refresh_jti: refresh_jti.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn mark_refresh_token_used( + &self, + refresh_jti: &str, + session_id: tranquil_db_traits::SessionId, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::MarkRefreshTokenUsed { + refresh_jti: refresh_jti.to_owned(), + session_id, + tx, + }, + ))?; + recv(rx).await + } + + async fn list_app_passwords( + &self, + user_id: Uuid, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::ListAppPasswords { user_id, tx }, + ))?; + recv(rx).await + } + + async fn get_app_passwords_for_login( + &self, + user_id: Uuid, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetAppPasswordsForLogin { user_id, tx }, + ))?; + recv(rx).await + } + + async fn get_app_password_by_name( + &self, + user_id: Uuid, + name: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetAppPasswordByName { + user_id, + name: name.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn create_app_password( + &self, + data: &tranquil_db_traits::AppPasswordCreate, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::CreateAppPassword { + data: data.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::DeleteAppPassword { + user_id, + name: name.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_app_passwords_by_controller( + &self, + did: &Did, + controller_did: &Did, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::DeleteAppPasswordsByController { + did: did.clone(), + controller_did: controller_did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_last_reauth_at(&self, did: &Did) -> Result>, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Session(SessionRequest::GetLastReauthAt { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn update_last_reauth(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::UpdateLastReauth { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_session_mfa_status( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetSessionMfaStatus { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn update_mfa_verified(&self, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::UpdateMfaVerified { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::GetAppPasswordHashesByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn refresh_session_atomic( + &self, + data: &tranquil_db_traits::SessionRefreshData, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::RefreshSessionAtomic { + data: data.clone(), + tx, + }, + ))?; + recv(rx).await + } +} + +#[async_trait] +impl tranquil_db_traits::InfraRepository for MetastoreClient { + async fn enqueue_comms( + &self, + user_id: Option, + channel: CommsChannel, + comms_type: CommsType, + recipient: &str, + subject: Option<&str>, + body: &str, + metadata: Option, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::EnqueueComms { + user_id, + channel, + comms_type, + recipient: recipient.to_owned(), + subject: subject.map(str::to_owned), + body: body.to_owned(), + metadata, + tx, + }))?; + recv(rx).await + } + + async fn fetch_pending_comms( + &self, + now: DateTime, + batch_size: i64, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::FetchPendingComms { + now, + batch_size, + tx, + }))?; + recv(rx).await + } + + async fn mark_comms_sent(&self, id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::MarkCommsSent { + id, + tx, + }))?; + recv(rx).await + } + + async fn mark_comms_failed(&self, id: Uuid, error: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::MarkCommsFailed { + id, + error: error.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn create_invite_code( + &self, + code: &str, + use_count: i32, + for_account: Option<&Did>, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::CreateInviteCode { + code: code.to_owned(), + use_count, + for_account: for_account.cloned(), + tx, + }))?; + recv(rx).await + } + + async fn create_invite_codes_batch( + &self, + codes: &[String], + use_count: i32, + created_by_user: Uuid, + for_account: Option<&Did>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::CreateInviteCodesBatch { + codes: codes.to_vec(), + use_count, + created_by_user, + for_account: for_account.cloned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_invite_code_available_uses(&self, code: &str) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetInviteCodeAvailableUses { + code: code.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn validate_invite_code<'a>( + &self, + code: &'a str, + ) -> Result, InviteCodeError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::ValidateInviteCode { + code: code.to_owned(), + tx, + })) + .map_err(|e| InviteCodeError::DatabaseError(DbError::Connection(e.to_string())))?; + recv_invite(rx).await?; + Ok(ValidatedInviteCode::new_validated(code)) + } + + async fn decrement_invite_code_uses( + &self, + code: &ValidatedInviteCode<'_>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DecrementInviteCodeUses { + code: code.code().to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn record_invite_code_use( + &self, + code: &ValidatedInviteCode<'_>, + used_by_user: Uuid, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::RecordInviteCodeUse { + code: code.code().to_owned(), + used_by_user, + tx, + }))?; + recv(rx).await + } + + async fn get_invite_codes_for_account( + &self, + for_account: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetInviteCodesForAccount { + for_account: for_account.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_invite_code_uses(&self, code: &str) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::GetInviteCodeUses { + code: code.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn disable_invite_codes_by_code(&self, codes: &[String]) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DisableInviteCodesByCode { + codes: codes.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn disable_invite_codes_by_account(&self, accounts: &[Did]) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DisableInviteCodesByAccount { + accounts: accounts.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn list_invite_codes( + &self, + cursor: Option<&str>, + limit: i64, + sort: InviteCodeSortOrder, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::ListInviteCodes { + cursor: cursor.map(str::to_owned), + limit, + sort, + tx, + }))?; + recv(rx).await + } + + async fn get_user_dids_by_ids(&self, user_ids: &[Uuid]) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::GetUserDidsByIds { + user_ids: user_ids.to_vec(), + tx, + }))?; + recv(rx).await + } + + async fn get_invite_code_uses_batch( + &self, + codes: &[String], + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetInviteCodeUsesBatch { + codes: codes.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_invites_created_by_user( + &self, + user_id: Uuid, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetInvitesCreatedByUser { user_id, tx }, + ))?; + recv(rx).await + } + + async fn get_invite_code_info(&self, code: &str) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::GetInviteCodeInfo { + code: code.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_invite_codes_by_users( + &self, + user_ids: &[Uuid], + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetInviteCodesByUsers { + user_ids: user_ids.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetInviteCodeUsedByUser { user_id, tx }, + ))?; + recv(rx).await + } + + async fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DeleteInviteCodeUsesByUser { user_id, tx }, + ))?; + recv(rx).await + } + + async fn delete_invite_codes_by_user(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DeleteInviteCodesByUser { user_id, tx }, + ))?; + recv(rx).await + } + + async fn reserve_signing_key( + &self, + did: Option<&Did>, + public_key_did_key: &str, + private_key_bytes: &[u8], + expires_at: DateTime, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::ReserveSigningKey { + did: did.cloned(), + public_key_did_key: public_key_did_key.to_owned(), + private_key_bytes: private_key_bytes.to_vec(), + expires_at, + tx, + }))?; + recv(rx).await + } + + async fn get_reserved_signing_key( + &self, + public_key_did_key: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetReservedSigningKey { + public_key_did_key: public_key_did_key.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn mark_signing_key_used(&self, key_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::MarkSigningKeyUsed { + key_id, + tx, + }))?; + recv(rx).await + } + + async fn create_deletion_request( + &self, + token: &str, + did: &Did, + expires_at: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::CreateDeletionRequest { + token: token.to_owned(), + did: did.clone(), + expires_at, + tx, + }, + ))?; + recv(rx).await + } + + async fn get_deletion_request(&self, token: &str) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::GetDeletionRequest { + token: token.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn delete_deletion_request(&self, token: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DeleteDeletionRequest { + token: token.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_deletion_requests_by_did(&self, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DeleteDeletionRequestsByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn upsert_account_preference( + &self, + user_id: Uuid, + name: &str, + value_json: serde_json::Value, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::UpsertAccountPreference { + user_id, + name: name.to_owned(), + value_json, + tx, + }, + ))?; + recv(rx).await + } + + async fn insert_account_preference_if_not_exists( + &self, + user_id: Uuid, + name: &str, + value_json: serde_json::Value, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::InsertAccountPreferenceIfNotExists { + user_id, + name: name.to_owned(), + value_json, + tx, + }, + ))?; + recv(rx).await + } + + async fn get_server_config(&self, key: &str) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::GetServerConfig { + key: key.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn health_check(&self) -> Result { + Ok(true) + } + + async fn insert_report( + &self, + id: i64, + reason_type: &str, + reason: Option<&str>, + subject_json: serde_json::Value, + reported_by_did: &Did, + created_at: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::InsertReport { + id, + reason_type: reason_type.to_owned(), + reason: reason.map(str::to_owned), + subject_json, + reported_by_did: reported_by_did.clone(), + created_at, + tx, + }))?; + recv(rx).await + } + + async fn delete_plc_tokens_for_user(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::DeletePlcTokensForUser { user_id, tx }, + ))?; + recv(rx).await + } + + async fn insert_plc_token( + &self, + user_id: Uuid, + token: &str, + expires_at: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::InsertPlcToken { + user_id, + token: token.to_owned(), + expires_at, + tx, + }))?; + recv(rx).await + } + + async fn get_plc_token_expiry( + &self, + user_id: Uuid, + token: &str, + ) -> Result>, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::GetPlcTokenExpiry { + user_id, + token: token.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn delete_plc_token(&self, user_id: Uuid, token: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::DeletePlcToken { + user_id, + token: token.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_account_preferences( + &self, + user_id: Uuid, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetAccountPreferences { user_id, tx }, + ))?; + recv(rx).await + } + + async fn replace_namespace_preferences( + &self, + user_id: Uuid, + namespace: &str, + preferences: Vec<(String, serde_json::Value)>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::ReplaceNamespacePreferences { + user_id, + namespace: namespace.to_owned(), + preferences, + tx, + }, + ))?; + recv(rx).await + } + + async fn get_notification_history( + &self, + user_id: Uuid, + limit: i64, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetNotificationHistory { user_id, limit, tx }, + ))?; + recv(rx).await + } + + async fn get_server_configs(&self, keys: &[&str]) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::GetServerConfigs { + keys: keys.iter().map(|s| (*s).to_owned()).collect(), + tx, + }))?; + recv(rx).await + } + + async fn upsert_server_config(&self, key: &str, value: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::UpsertServerConfig { + key: key.to_owned(), + value: value.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn delete_server_config(&self, key: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::DeleteServerConfig { + key: key.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_blob_storage_key_by_cid(&self, cid: &CidLink) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetBlobStorageKeyByCid { + cid: cid.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_blob_by_cid(&self, cid: &CidLink) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::Infra(InfraRequest::DeleteBlobByCid { + cid: cid.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_admin_account_info_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetAdminAccountInfoByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_admin_account_infos_by_dids( + &self, + dids: &[Did], + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetAdminAccountInfosByDids { + dids: dids.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_invite_code_uses_by_users( + &self, + user_ids: &[Uuid], + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Infra( + InfraRequest::GetInviteCodeUsesByUsers { + user_ids: user_ids.to_vec(), + tx, + }, + ))?; + recv(rx).await + } +} + +#[async_trait] +impl tranquil_db_traits::OAuthRepository for MetastoreClient { + async fn create_token(&self, data: &TokenData) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::CreateToken { + data: data.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_token_by_id(&self, token_id: &TokenId) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::GetTokenById { + token_id: token_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_token_by_refresh_token( + &self, + refresh_token: &RefreshToken, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::GetTokenByRefreshToken { + refresh_token: refresh_token.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_token_by_previous_refresh_token( + &self, + refresh_token: &RefreshToken, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::GetTokenByPreviousRefreshToken { + refresh_token: refresh_token.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn rotate_token( + &self, + old_db_id: TokenFamilyId, + new_refresh_token: &RefreshToken, + new_expires_at: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::RotateToken { + old_db_id, + new_refresh_token: new_refresh_token.clone(), + new_expires_at, + tx, + }))?; + recv(rx).await + } + + async fn check_refresh_token_used( + &self, + refresh_token: &RefreshToken, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::CheckRefreshTokenUsed { + refresh_token: refresh_token.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_token(&self, token_id: &TokenId) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::DeleteToken { + token_id: token_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn delete_token_family(&self, db_id: TokenFamilyId) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::DeleteTokenFamily { + db_id, + tx, + }))?; + recv(rx).await + } + + async fn list_tokens_for_user(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::ListTokensForUser { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn count_tokens_for_user(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::CountTokensForUser { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn delete_oldest_tokens_for_user( + &self, + did: &Did, + keep_count: i64, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::DeleteOldestTokensForUser { + did: did.clone(), + keep_count, + tx, + }, + ))?; + recv(rx).await + } + + async fn revoke_tokens_for_client( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::RevokeTokensForClient { + did: did.clone(), + client_id: client_id.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn revoke_tokens_for_controller( + &self, + delegated_did: &Did, + controller_did: &Did, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::RevokeTokensForController { + delegated_did: delegated_did.clone(), + controller_did: controller_did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn create_authorization_request( + &self, + request_id: &RequestId, + data: &RequestData, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::CreateAuthorizationRequest { + request_id: request_id.clone(), + data: data.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_authorization_request( + &self, + request_id: &RequestId, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::GetAuthorizationRequest { + request_id: request_id.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn set_authorization_did( + &self, + request_id: &RequestId, + did: &Did, + device_id: Option<&DeviceId>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::SetAuthorizationDid { + request_id: request_id.clone(), + did: did.clone(), + device_id: device_id.cloned(), + tx, + }))?; + recv(rx).await + } + + async fn update_authorization_request( + &self, + request_id: &RequestId, + did: &Did, + device_id: Option<&DeviceId>, + code: &AuthorizationCode, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::UpdateAuthorizationRequest { + request_id: request_id.clone(), + did: did.clone(), + device_id: device_id.cloned(), + code: code.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn consume_authorization_request_by_code( + &self, + code: &AuthorizationCode, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::ConsumeAuthorizationRequestByCode { + code: code.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_authorization_request(&self, request_id: &RequestId) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::DeleteAuthorizationRequest { + request_id: request_id.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_expired_authorization_requests(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::DeleteExpiredAuthorizationRequests { tx }, + ))?; + recv(rx).await + } + + async fn extend_authorization_request_expiry( + &self, + request_id: &RequestId, + new_expires_at: DateTime, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::ExtendAuthorizationRequestExpiry { + request_id: request_id.clone(), + new_expires_at, + tx, + }, + ))?; + recv(rx).await + } + + async fn mark_request_authenticated( + &self, + request_id: &RequestId, + did: &Did, + device_id: Option<&DeviceId>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::MarkRequestAuthenticated { + request_id: request_id.clone(), + did: did.clone(), + device_id: device_id.cloned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn update_request_scope( + &self, + request_id: &RequestId, + scope: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::UpdateRequestScope { + request_id: request_id.clone(), + scope: scope.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn set_controller_did( + &self, + request_id: &RequestId, + controller_did: &Did, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::SetControllerDid { + request_id: request_id.clone(), + controller_did: controller_did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn set_request_did(&self, request_id: &RequestId, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::SetRequestDid { + request_id: request_id.clone(), + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn create_device(&self, device_id: &DeviceId, data: &DeviceData) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::CreateDevice { + device_id: device_id.clone(), + data: data.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_device(&self, device_id: &DeviceId) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::GetDevice { + device_id: device_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn update_device_last_seen(&self, device_id: &DeviceId) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::UpdateDeviceLastSeen { + device_id: device_id.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_device(&self, device_id: &DeviceId) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::DeleteDevice { + device_id: device_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn upsert_account_device(&self, did: &Did, device_id: &DeviceId) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::UpsertAccountDevice { + did: did.clone(), + device_id: device_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_device_accounts( + &self, + device_id: &DeviceId, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::GetDeviceAccounts { + device_id: device_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn verify_account_on_device( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::VerifyAccountOnDevice { + device_id: device_id.clone(), + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn check_and_record_dpop_jti(&self, jti: &DPoPProofId) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::CheckAndRecordDpopJti { + jti: jti.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn cleanup_expired_dpop_jtis(&self, max_age_secs: i64) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::CleanupExpiredDpopJtis { max_age_secs, tx }, + ))?; + recv(rx).await + } + + async fn create_2fa_challenge( + &self, + did: &Did, + request_uri: &RequestId, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::Create2faChallenge { + did: did.clone(), + request_uri: request_uri.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_2fa_challenge( + &self, + request_uri: &RequestId, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::Get2faChallenge { + request_uri: request_uri.clone(), + tx, + }))?; + recv(rx).await + } + + async fn increment_2fa_attempts(&self, id: Uuid) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::Increment2faAttempts { id, tx }, + ))?; + recv(rx).await + } + + async fn delete_2fa_challenge(&self, id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::Delete2faChallenge { + id, + tx, + }))?; + recv(rx).await + } + + async fn delete_2fa_challenge_by_request_uri( + &self, + request_uri: &RequestId, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::Delete2faChallengeByRequestUri { + request_uri: request_uri.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn cleanup_expired_2fa_challenges(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::CleanupExpired2faChallenges { tx }, + ))?; + recv(rx).await + } + + async fn check_user_2fa_enabled(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::CheckUser2faEnabled { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_scope_preferences( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::GetScopePreferences { + did: did.clone(), + client_id: client_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn upsert_scope_preferences( + &self, + did: &Did, + client_id: &ClientId, + prefs: &[ScopePreference], + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::UpsertScopePreferences { + did: did.clone(), + client_id: client_id.clone(), + prefs: prefs.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_scope_preferences( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::DeleteScopePreferences { + did: did.clone(), + client_id: client_id.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn upsert_authorized_client( + &self, + did: &Did, + client_id: &ClientId, + data: &AuthorizedClientData, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::UpsertAuthorizedClient { + did: did.clone(), + client_id: client_id.clone(), + data: data.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_authorized_client( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::GetAuthorizedClient { + did: did.clone(), + client_id: client_id.clone(), + tx, + }))?; + recv(rx).await + } + + async fn list_trusted_devices( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::ListTrustedDevices { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_device_trust_info( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::GetDeviceTrustInfo { + device_id: device_id.clone(), + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn device_belongs_to_user( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::DeviceBelongsToUser { + device_id: device_id.clone(), + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn revoke_device_trust(&self, device_id: &DeviceId, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::RevokeDeviceTrust { + device_id: device_id.clone(), + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn update_device_friendly_name( + &self, + device_id: &DeviceId, + did: &Did, + friendly_name: Option<&str>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::UpdateDeviceFriendlyName { + device_id: device_id.clone(), + did: did.clone(), + friendly_name: friendly_name.map(str::to_owned), + tx, + }, + ))?; + recv(rx).await + } + + async fn trust_device( + &self, + device_id: &DeviceId, + did: &Did, + trusted_at: DateTime, + trusted_until: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::TrustDevice { + device_id: device_id.clone(), + did: did.clone(), + trusted_at, + trusted_until, + tx, + }))?; + recv(rx).await + } + + async fn extend_device_trust( + &self, + device_id: &DeviceId, + did: &Did, + trusted_until: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::ExtendDeviceTrust { + device_id: device_id.clone(), + did: did.clone(), + trusted_until, + tx, + }))?; + recv(rx).await + } + + async fn list_sessions_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::ListSessionsByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn delete_session_by_id( + &self, + session_id: TokenFamilyId, + did: &Did, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::DeleteSessionById { + session_id, + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn delete_sessions_by_did(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::OAuth(OAuthRequest::DeleteSessionsByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn delete_sessions_by_did_except( + &self, + did: &Did, + except_token_id: &TokenId, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::OAuth( + OAuthRequest::DeleteSessionsByDidExcept { + did: did.clone(), + except_token_id: except_token_id.clone(), + tx, + }, + ))?; + recv(rx).await + } +} + +#[async_trait] +impl tranquil_db_traits::UserRepository for MetastoreClient { + async fn get_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_by_handle(&self, handle: &Handle) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetByHandle { + handle: handle.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_with_key_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetWithKeyByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_status_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetStatusByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn count_users(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CountUsers { tx }))?; + recv(rx).await + } + + async fn get_session_access_expiry( + &self, + did: &Did, + access_jti: &str, + ) -> Result>, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetSessionAccessExpiry { + did: did.clone(), + access_jti: access_jti.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_oauth_token_with_user( + &self, + token_id: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetOAuthTokenWithUser { + token_id: token_id.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_user_info_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserInfoByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_any_admin_user_id(&self) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetAnyAdminUserId { + tx, + }))?; + recv(rx).await + } + + async fn set_invites_disabled(&self, did: &Did, disabled: bool) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetInvitesDisabled { + did: did.clone(), + disabled, + tx, + }))?; + recv(rx).await + } + + async fn search_accounts( + &self, + cursor_did: Option<&Did>, + email_filter: Option<&str>, + handle_filter: Option<&str>, + limit: i64, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SearchAccounts { + cursor_did: cursor_did.cloned(), + email_filter: email_filter.map(str::to_owned), + handle_filter: handle_filter.map(str::to_owned), + limit, + tx, + }))?; + recv(rx).await + } + + async fn get_auth_info_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetAuthInfoByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_by_email(&self, email: &str) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetByEmail { + email: email.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_login_check_by_handle_or_email( + &self, + identifier: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetLoginCheckByHandleOrEmail { + identifier: identifier.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_login_info_by_handle_or_email( + &self, + identifier: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetLoginInfoByHandleOrEmail { + identifier: identifier.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_2fa_status_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::Get2faStatusByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_comms_prefs(&self, user_id: Uuid) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetCommsPrefs { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn get_id_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetIdByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_user_key_by_id(&self, user_id: Uuid) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserKeyById { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn get_id_and_handle_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetIdAndHandleByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_did_web_info_by_handle( + &self, + handle: &Handle, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetDidWebInfoByHandle { + handle: handle.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_did_web_overrides( + &self, + user_id: Uuid, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetDidWebOverrides { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn get_handle_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetHandleByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn is_account_active_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::IsAccountActiveByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_user_for_deletion(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserForDeletion { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn check_handle_exists( + &self, + handle: &Handle, + exclude_user_id: Uuid, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CheckHandleExists { + handle: handle.clone(), + exclude_user_id, + tx, + }))?; + recv(rx).await + } + + async fn update_handle(&self, user_id: Uuid, handle: &Handle) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdateHandle { + user_id, + handle: handle.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_user_with_key_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserWithKeyByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn is_account_migrated(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::IsAccountMigrated { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn has_verified_comms_channel(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::HasVerifiedCommsChannel { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_id_by_handle(&self, handle: &Handle) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetIdByHandle { + handle: handle.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_email_info_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetEmailInfoByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn check_email_exists( + &self, + email: &str, + exclude_user_id: Uuid, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CheckEmailExists { + email: email.to_owned(), + exclude_user_id, + tx, + }))?; + recv(rx).await + } + + async fn update_email(&self, user_id: Uuid, email: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdateEmail { + user_id, + email: email.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn set_email_verified(&self, user_id: Uuid, verified: bool) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetEmailVerified { + user_id, + verified, + tx, + }))?; + recv(rx).await + } + + async fn check_email_verified_by_identifier( + &self, + identifier: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::CheckEmailVerifiedByIdentifier { + identifier: identifier.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn check_channel_verified_by_did( + &self, + did: &Did, + channel: CommsChannel, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::CheckChannelVerifiedByDid { + did: did.clone(), + channel, + tx, + }, + ))?; + recv(rx).await + } + + async fn admin_update_email(&self, did: &Did, email: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::AdminUpdateEmail { + did: did.clone(), + email: email.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn admin_update_handle(&self, did: &Did, handle: &Handle) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::AdminUpdateHandle { + did: did.clone(), + handle: handle.clone(), + tx, + }))?; + recv(rx).await + } + + async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::AdminUpdatePassword { + did: did.clone(), + password_hash: password_hash.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_notification_prefs( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetNotificationPrefs { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_id_handle_email_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetIdHandleEmailByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn update_preferred_comms_channel( + &self, + did: &Did, + channel: CommsChannel, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::UpdatePreferredCommsChannel { + did: did.clone(), + channel, + tx, + }, + ))?; + recv(rx).await + } + + async fn clear_discord(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::ClearDiscord { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn clear_telegram(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::ClearTelegram { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn clear_signal(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::ClearSignal { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn set_unverified_signal( + &self, + user_id: Uuid, + signal_username: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetUnverifiedSignal { + user_id, + signal_username: signal_username.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn set_unverified_telegram( + &self, + user_id: Uuid, + telegram_username: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetUnverifiedTelegram { + user_id, + telegram_username: telegram_username.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn store_telegram_chat_id( + &self, + telegram_username: &str, + chat_id: i64, + handle: Option<&str>, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::StoreTelegramChatId { + telegram_username: telegram_username.to_owned(), + chat_id, + handle: handle.map(str::to_owned), + tx, + }))?; + recv(rx).await + } + + async fn get_telegram_chat_id(&self, user_id: Uuid) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetTelegramChatId { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn set_unverified_discord( + &self, + user_id: Uuid, + discord_username: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetUnverifiedDiscord { + user_id, + discord_username: discord_username.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn store_discord_user_id( + &self, + discord_username: &str, + discord_id: &str, + handle: Option<&str>, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::StoreDiscordUserId { + discord_username: discord_username.to_owned(), + discord_id: discord_id.to_owned(), + handle: handle.map(str::to_owned), + tx, + }))?; + recv(rx).await + } + + async fn get_verification_info( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetVerificationInfo { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn verify_email_channel(&self, user_id: Uuid, email: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::VerifyEmailChannel { + user_id, + email: email.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn verify_discord_channel(&self, user_id: Uuid, discord_id: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::VerifyDiscordChannel { + user_id, + discord_id: discord_id.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn verify_telegram_channel( + &self, + user_id: Uuid, + telegram_username: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::VerifyTelegramChannel { + user_id, + telegram_username: telegram_username.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn verify_signal_channel( + &self, + user_id: Uuid, + signal_username: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::VerifySignalChannel { + user_id, + signal_username: signal_username.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn set_email_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetEmailVerifiedFlag { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn set_discord_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::SetDiscordVerifiedFlag { user_id, tx }, + ))?; + recv(rx).await + } + + async fn set_telegram_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::SetTelegramVerifiedFlag { user_id, tx }, + ))?; + recv(rx).await + } + + async fn set_signal_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetSignalVerifiedFlag { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn has_totp_enabled(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::HasTotpEnabled { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn has_passkeys(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::HasPasskeys { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_password_hash_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetPasswordHashByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_passkeys_for_user(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetPasskeysForUser { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_passkey_by_credential_id( + &self, + credential_id: &[u8], + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetPasskeyByCredentialId { + credential_id: credential_id.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn save_passkey( + &self, + did: &Did, + credential_id: &[u8], + public_key: &[u8], + friendly_name: Option<&str>, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SavePasskey { + did: did.clone(), + credential_id: credential_id.to_vec(), + public_key: public_key.to_vec(), + friendly_name: friendly_name.map(str::to_owned), + tx, + }))?; + recv(rx).await + } + + async fn update_passkey_counter( + &self, + credential_id: &[u8], + new_counter: i32, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdatePasskeyCounter { + credential_id: credential_id.to_vec(), + new_counter, + tx, + }))?; + recv(rx).await + } + + async fn delete_passkey(&self, id: Uuid, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::DeletePasskey { + id, + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn update_passkey_name(&self, id: Uuid, did: &Did, name: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdatePasskeyName { + id, + did: did.clone(), + name: name.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn save_webauthn_challenge( + &self, + did: &Did, + challenge_type: WebauthnChallengeType, + state_json: &str, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SaveWebauthnChallenge { + did: did.clone(), + challenge_type, + state_json: state_json.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn load_webauthn_challenge( + &self, + did: &Did, + challenge_type: WebauthnChallengeType, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::LoadWebauthnChallenge { + did: did.clone(), + challenge_type, + tx, + }))?; + recv(rx).await + } + + async fn delete_webauthn_challenge( + &self, + did: &Did, + challenge_type: WebauthnChallengeType, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::DeleteWebauthnChallenge { + did: did.clone(), + challenge_type, + tx, + }, + ))?; + recv(rx).await + } + + async fn get_totp_record(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetTotpRecord { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_totp_record_state(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetTotpRecordState { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn upsert_totp_secret( + &self, + did: &Did, + secret_encrypted: &[u8], + encryption_version: i32, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpsertTotpSecret { + did: did.clone(), + secret_encrypted: secret_encrypted.to_vec(), + encryption_version, + tx, + }))?; + recv(rx).await + } + + async fn set_totp_verified(&self, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetTotpVerified { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn update_totp_last_used(&self, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdateTotpLastUsed { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn delete_totp(&self, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::DeleteTotp { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_unused_backup_codes(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUnusedBackupCodes { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn mark_backup_code_used(&self, code_id: Uuid) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::MarkBackupCodeUsed { + code_id, + tx, + }))?; + recv(rx).await + } + + async fn count_unused_backup_codes(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::CountUnusedBackupCodes { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_backup_codes(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::DeleteBackupCodes { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn insert_backup_codes(&self, did: &Did, code_hashes: &[String]) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::InsertBackupCodes { + did: did.clone(), + code_hashes: code_hashes.to_vec(), + tx, + }))?; + recv(rx).await + } + + async fn enable_totp_with_backup_codes( + &self, + did: &Did, + code_hashes: &[String], + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::EnableTotpWithBackupCodes { + did: did.clone(), + code_hashes: code_hashes.to_vec(), + tx, + }, + ))?; + recv(rx).await + } + + async fn delete_totp_and_backup_codes(&self, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::DeleteTotpAndBackupCodes { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn replace_backup_codes(&self, did: &Did, code_hashes: &[String]) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::ReplaceBackupCodes { + did: did.clone(), + code_hashes: code_hashes.to_vec(), + tx, + }))?; + recv(rx).await + } + + async fn get_session_info_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetSessionInfoByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_legacy_login_pref( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetLegacyLoginPref { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn update_legacy_login(&self, did: &Did, allow: bool) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdateLegacyLogin { + did: did.clone(), + allow, + tx, + }))?; + recv(rx).await + } + + async fn update_locale(&self, did: &Did, locale: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdateLocale { + did: did.clone(), + locale: locale.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_login_full_by_identifier( + &self, + identifier: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetLoginFullByIdentifier { + identifier: identifier.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_confirm_signup_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetConfirmSignupByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_resend_verification_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetResendVerificationByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn set_channel_verified(&self, did: &Did, channel: CommsChannel) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetChannelVerified { + did: did.clone(), + channel, + tx, + }))?; + recv(rx).await + } + + async fn get_id_by_email_or_handle( + &self, + email: &str, + handle: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetIdByEmailOrHandle { + email: email.to_owned(), + handle: handle.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn count_accounts_by_email(&self, email: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CountAccountsByEmail { + email: email.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_handles_by_email(&self, email: &str) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetHandlesByEmail { + email: email.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn set_password_reset_code( + &self, + user_id: Uuid, + code: &str, + expires_at: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetPasswordResetCode { + user_id, + code: code.to_owned(), + expires_at, + tx, + }))?; + recv(rx).await + } + + async fn get_user_by_reset_code( + &self, + code: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserByResetCode { + code: code.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn clear_password_reset_code(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::ClearPasswordResetCode { user_id, tx }, + ))?; + recv(rx).await + } + + async fn get_id_and_password_hash_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetIdAndPasswordHashByDid { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn update_password_hash( + &self, + user_id: Uuid, + password_hash: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdatePasswordHash { + user_id, + password_hash: password_hash.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn reset_password_with_sessions( + &self, + user_id: Uuid, + password_hash: &str, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::ResetPasswordWithSessions { + user_id, + password_hash: password_hash.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn activate_account(&self, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::ActivateAccount { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn deactivate_account( + &self, + did: &Did, + delete_after: Option>, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::DeactivateAccount { + did: did.clone(), + delete_after, + tx, + }))?; + recv(rx).await + } + + async fn has_password_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::HasPasswordByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_password_info_by_did( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetPasswordInfoByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn remove_user_password(&self, user_id: Uuid) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::RemoveUserPassword { + user_id, + tx, + }))?; + recv(rx).await + } + + async fn set_new_user_password( + &self, + user_id: Uuid, + password_hash: &str, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetNewUserPassword { + user_id, + password_hash: password_hash.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_user_key_by_did(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserKeyByDid { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::DeleteAccountComplete { + user_id, + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn set_user_takedown( + &self, + did: &Did, + takedown_ref: Option<&str>, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetUserTakedown { + did: did.clone(), + takedown_ref: takedown_ref.map(str::to_owned), + tx, + }))?; + recv(rx).await + } + + async fn admin_delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::AdminDeleteAccountComplete { + user_id, + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_user_for_did_doc(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserForDidDoc { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_user_for_did_doc_build( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserForDidDocBuild { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn upsert_did_web_overrides( + &self, + user_id: Uuid, + verification_methods: Option, + also_known_as: Option>, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpsertDidWebOverrides { + user_id, + verification_methods, + also_known_as, + tx, + }))?; + recv(rx).await + } + + async fn update_migrated_to_pds(&self, did: &Did, endpoint: &str) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::UpdateMigratedToPds { + did: did.clone(), + endpoint: endpoint.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn get_user_for_passkey_setup( + &self, + did: &Did, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetUserForPasskeySetup { + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn get_user_for_passkey_recovery( + &self, + identifier: &str, + normalized_handle: &str, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetUserForPasskeyRecovery { + identifier: identifier.to_owned(), + normalized_handle: normalized_handle.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn set_recovery_token( + &self, + did: &Did, + token_hash: &str, + expires_at: DateTime, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::SetRecoveryToken { + did: did.clone(), + token_hash: token_hash.to_owned(), + expires_at, + tx, + }))?; + recv(rx).await + } + + async fn get_user_for_recovery(&self, did: &Did) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::GetUserForRecovery { + did: did.clone(), + tx, + }))?; + recv(rx).await + } + + async fn get_accounts_scheduled_for_deletion( + &self, + limit: i64, + ) -> Result, DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::GetAccountsScheduledForDeletion { limit, tx }, + ))?; + recv(rx).await + } + + async fn delete_account_with_firehose(&self, user_id: Uuid, did: &Did) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::DeleteAccountWithFirehose { + user_id, + did: did.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn create_password_account( + &self, + input: &CreatePasswordAccountInput, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CreatePasswordAccount { + input: input.clone(), + tx, + })) + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + recv_create_account(rx).await + } + + async fn create_delegated_account( + &self, + input: &CreateDelegatedAccountInput, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User( + UserRequest::CreateDelegatedAccount { + input: input.clone(), + tx, + }, + )) + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + recv_create_account(rx).await + } + + async fn create_passkey_account( + &self, + input: &CreatePasskeyAccountInput, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CreatePasskeyAccount { + input: input.clone(), + tx, + })) + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + recv_create_account(rx).await + } + + async fn create_sso_account( + &self, + input: &CreateSsoAccountInput, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CreateSsoAccount { + input: input.clone(), + tx, + })) + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + recv_create_account(rx).await + } + + async fn reactivate_migration_account( + &self, + input: &MigrationReactivationInput, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User( + UserRequest::ReactivateMigrationAccount { + input: input.clone(), + tx, + }, + )) + .map_err(|e| MigrationReactivationError::Database(e.to_string()))?; + recv_migration_reactivation(rx).await + } + + async fn check_handle_available_for_new_account( + &self, + handle: &Handle, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::CheckHandleAvailableForNewAccount { + handle: handle.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn reserve_handle(&self, handle: &Handle, reserved_by: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::ReserveHandle { + handle: handle.clone(), + reserved_by: reserved_by.to_owned(), + tx, + }))?; + recv(rx).await + } + + async fn release_handle_reservation(&self, handle: &Handle) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::ReleaseHandleReservation { + handle: handle.clone(), + tx, + }, + ))?; + recv(rx).await + } + + async fn cleanup_expired_handle_reservations(&self) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::CleanupExpiredHandleReservations { tx }, + ))?; + recv(rx).await + } + + async fn check_and_consume_invite_code(&self, code: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::User( + UserRequest::CheckAndConsumeInviteCode { + code: code.to_owned(), + tx, + }, + ))?; + recv(rx).await + } + + async fn complete_passkey_setup( + &self, + input: &CompletePasskeySetupInput, + ) -> Result<(), DbError> { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::CompletePasskeySetup { + input: input.clone(), + tx, + }))?; + recv(rx).await + } + + async fn recover_passkey_account( + &self, + input: &RecoverPasskeyAccountInput, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool + .send(MetastoreRequest::User(UserRequest::RecoverPasskeyAccount { + input: input.clone(), + tx, + }))?; + recv(rx).await + } +} diff --git a/crates/tranquil-store/src/metastore/commit_ops.rs b/crates/tranquil-store/src/metastore/commit_ops.rs index 5bcd64b..ad7e774 100644 --- a/crates/tranquil-store/src/metastore/commit_ops.rs +++ b/crates/tranquil-store/src/metastore/commit_ops.rs @@ -367,7 +367,7 @@ impl CommitOps { &self, limit: i64, ) -> Result, MetastoreError> { - let limit_usize = usize::try_from(limit).unwrap_or(usize::MAX); + let limit_usize = usize::try_from(limit).unwrap_or(0); self.scan_users_missing_prefix( record_blobs_user_prefix, diff --git a/crates/tranquil-store/src/metastore/delegation_ops.rs b/crates/tranquil-store/src/metastore/delegation_ops.rs new file mode 100644 index 0000000..7740249 --- /dev/null +++ b/crates/tranquil-store/src/metastore/delegation_ops.rs @@ -0,0 +1,412 @@ +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use fjall::{Database, Keyspace}; +use uuid::Uuid; + +use super::MetastoreError; +use super::delegations::{ + AuditLogValue, DelegationGrantValue, action_type_to_u8, audit_log_key, audit_log_prefix, + by_controller_key, by_controller_prefix, grant_key, grant_prefix, u8_to_action_type, +}; +use super::keys::UserHash; +use super::scan::{count_prefix, point_lookup}; +use super::user_hash::UserHashMap; + +use tranquil_db_traits::DbScope; +use tranquil_db_traits::{ + AuditLogEntry, ControllerInfo, DelegatedAccountInfo, DelegationActionType, DelegationGrant, +}; +use tranquil_types::{Did, Handle}; + +pub struct DelegationOps { + db: Database, + indexes: Keyspace, + users: Keyspace, + user_hashes: Arc, +} + +impl DelegationOps { + pub fn new( + db: Database, + indexes: Keyspace, + users: Keyspace, + user_hashes: Arc, + ) -> Self { + Self { + db, + indexes, + users, + user_hashes, + } + } + + fn resolve_handle_for_did(&self, did_str: &str) -> Option { + let user_hash = UserHash::from_did(did_str); + let key = super::encoding::KeyBuilder::new() + .tag(super::keys::KeyTag::USER_PRIMARY) + .u64(user_hash.raw()) + .build(); + self.users + .get(key.as_slice()) + .ok() + .flatten() + .and_then(|raw| super::users::UserValue::deserialize(&raw)) + .and_then(|u| Handle::new(u.handle).ok()) + } + + fn value_to_grant(&self, v: &DelegationGrantValue) -> Result { + Ok(DelegationGrant { + id: v.id, + delegated_did: Did::new(v.delegated_did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?, + controller_did: Did::new(v.controller_did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid controller_did"))?, + granted_scopes: DbScope::new(&v.granted_scopes).unwrap_or_else(|_| DbScope::empty()), + granted_at: DateTime::from_timestamp_millis(v.granted_at_ms).unwrap_or_default(), + granted_by: Did::new(v.granted_by.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid granted_by"))?, + revoked_at: v.revoked_at_ms.and_then(DateTime::from_timestamp_millis), + revoked_by: v.revoked_by.as_ref().and_then(|d| Did::new(d.clone()).ok()), + }) + } + + pub fn is_delegated_account(&self, did: &Did) -> Result { + let delegated_hash = UserHash::from_did(did.as_str()); + let prefix = grant_prefix(delegated_hash); + let found = self + .indexes + .prefix(prefix.as_slice()) + .map(|guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + DelegationGrantValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt delegation grant")) + .map(|val| val.revoked_at_ms.is_none()) + }) + .find(|result| !matches!(result, Ok(false))); + match found { + Some(Ok(true)) => Ok(true), + Some(Err(e)) => Err(e), + _ => Ok(false), + } + } + + pub fn create_delegation( + &self, + delegated_did: &Did, + controller_did: &Did, + granted_scopes: &DbScope, + granted_by: &Did, + ) -> Result { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let controller_hash = UserHash::from_did(controller_did.as_str()); + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = DelegationGrantValue { + id, + delegated_did: delegated_did.to_string(), + controller_did: controller_did.to_string(), + granted_scopes: granted_scopes.as_str().to_owned(), + granted_at_ms: now_ms, + granted_by: granted_by.to_string(), + revoked_at_ms: None, + revoked_by: None, + }; + + let primary = grant_key(delegated_hash, controller_hash); + let reverse = by_controller_key(controller_hash, delegated_hash); + + let mut batch = self.db.batch(); + batch.insert(&self.indexes, primary.as_slice(), value.serialize()); + batch.insert(&self.indexes, reverse.as_slice(), []); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(id) + } + + pub fn revoke_delegation( + &self, + delegated_did: &Did, + controller_did: &Did, + revoked_by: &Did, + ) -> Result { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let controller_hash = UserHash::from_did(controller_did.as_str()); + let primary = grant_key(delegated_hash, controller_hash); + + let existing: Option = point_lookup( + &self.indexes, + primary.as_slice(), + DelegationGrantValue::deserialize, + "corrupt delegation grant", + )?; + + match existing { + Some(mut val) if val.revoked_at_ms.is_none() => { + val.revoked_at_ms = Some(Utc::now().timestamp_millis()); + val.revoked_by = Some(revoked_by.to_string()); + + let reverse = by_controller_key(controller_hash, delegated_hash); + let mut batch = self.db.batch(); + batch.insert(&self.indexes, primary.as_slice(), val.serialize()); + batch.remove(&self.indexes, reverse.as_slice()); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(true) + } + _ => Ok(false), + } + } + + pub fn update_delegation_scopes( + &self, + delegated_did: &Did, + controller_did: &Did, + new_scopes: &DbScope, + ) -> Result { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let controller_hash = UserHash::from_did(controller_did.as_str()); + let primary = grant_key(delegated_hash, controller_hash); + + let existing: Option = point_lookup( + &self.indexes, + primary.as_slice(), + DelegationGrantValue::deserialize, + "corrupt delegation grant", + )?; + + match existing { + Some(mut val) if val.revoked_at_ms.is_none() => { + val.granted_scopes = new_scopes.as_str().to_owned(); + self.indexes + .insert(primary.as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + _ => Ok(false), + } + } + + pub fn get_delegation( + &self, + delegated_did: &Did, + controller_did: &Did, + ) -> Result, MetastoreError> { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let controller_hash = UserHash::from_did(controller_did.as_str()); + let primary = grant_key(delegated_hash, controller_hash); + + let val: Option = point_lookup( + &self.indexes, + primary.as_slice(), + DelegationGrantValue::deserialize, + "corrupt delegation grant", + )?; + + val.map(|v| self.value_to_grant(&v)).transpose() + } + + pub fn get_delegations_for_account( + &self, + delegated_did: &Did, + ) -> Result, MetastoreError> { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let prefix = grant_prefix(delegated_hash); + + self.indexes + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = DelegationGrantValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt delegation grant"))?; + let is_active = val.revoked_at_ms.is_none(); + let controller_did_parsed = Did::new(val.controller_did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid controller_did"))?; + let handle = self.resolve_handle_for_did(&val.controller_did); + let is_local = self + .user_hashes + .get_uuid(&UserHash::from_did(&val.controller_did)) + .is_some(); + + acc.push(ControllerInfo { + did: controller_did_parsed, + handle, + granted_scopes: DbScope::new(&val.granted_scopes) + .unwrap_or_else(|_| DbScope::empty()), + granted_at: DateTime::from_timestamp_millis(val.granted_at_ms) + .unwrap_or_default(), + is_active, + is_local, + }); + Ok::<_, MetastoreError>(acc) + }) + } + + pub fn get_accounts_controlled_by( + &self, + controller_did: &Did, + ) -> Result, MetastoreError> { + let controller_hash = UserHash::from_did(controller_did.as_str()); + let prefix = by_controller_prefix(controller_hash); + + self.indexes + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (key_bytes, _) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let mut reader = super::encoding::KeyReader::new(&key_bytes); + let _tag = reader.tag(); + let _ctrl_hash = reader.u64(); + let deleg_hash_raw = reader + .u64() + .ok_or(MetastoreError::CorruptData("corrupt by_controller key"))?; + let deleg_hash = UserHash::from_raw(deleg_hash_raw); + + let grant_pfx = grant_key(deleg_hash, controller_hash); + let grant_val: Option = point_lookup( + &self.indexes, + grant_pfx.as_slice(), + DelegationGrantValue::deserialize, + "corrupt delegation grant", + )?; + + if let Some(val) = grant_val.filter(|v| v.revoked_at_ms.is_none()) { + let delegated_did = Did::new(val.delegated_did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?; + let handle = self + .resolve_handle_for_did(&val.delegated_did) + .unwrap_or_else(|| Handle::new("unknown.invalid").unwrap()); + acc.push(DelegatedAccountInfo { + did: delegated_did, + handle, + granted_scopes: DbScope::new(&val.granted_scopes) + .unwrap_or_else(|_| DbScope::empty()), + granted_at: DateTime::from_timestamp_millis(val.granted_at_ms) + .unwrap_or_default(), + }); + } + + Ok::<_, MetastoreError>(acc) + }) + } + + pub fn count_active_controllers(&self, delegated_did: &Did) -> Result { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let prefix = grant_prefix(delegated_hash); + + self.indexes + .prefix(prefix.as_slice()) + .try_fold(0i64, |acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = DelegationGrantValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt delegation grant"))?; + Ok::<_, MetastoreError>(match val.revoked_at_ms.is_none() { + true => acc.saturating_add(1), + false => acc, + }) + }) + } + + pub fn controls_any_accounts(&self, did: &Did) -> Result { + let controller_hash = UserHash::from_did(did.as_str()); + let prefix = by_controller_prefix(controller_hash); + match self.indexes.prefix(prefix.as_slice()).next() { + Some(guard) => { + guard.into_inner().map_err(MetastoreError::Fjall)?; + Ok(true) + } + None => Ok(false), + } + } + + #[allow(clippy::too_many_arguments)] + pub fn log_delegation_action( + &self, + delegated_did: &Did, + actor_did: &Did, + controller_did: Option<&Did>, + action_type: DelegationActionType, + action_details: Option, + ip_address: Option<&str>, + user_agent: Option<&str>, + ) -> Result { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = AuditLogValue { + id, + delegated_did: delegated_did.to_string(), + actor_did: actor_did.to_string(), + controller_did: controller_did.map(|d| d.to_string()), + action_type: action_type_to_u8(action_type), + action_details: action_details.map(|v| serde_json::to_vec(&v).unwrap_or_default()), + ip_address: ip_address.map(str::to_owned), + user_agent: user_agent.map(str::to_owned), + created_at_ms: now_ms, + }; + + let key = audit_log_key(delegated_hash, now_ms, id); + self.indexes + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall)?; + + Ok(id) + } + + pub fn get_audit_log_for_account( + &self, + delegated_did: &Did, + limit: i64, + offset: i64, + ) -> Result, MetastoreError> { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let prefix = audit_log_prefix(delegated_hash); + + let limit = usize::try_from(limit).unwrap_or(0); + let offset = usize::try_from(offset).unwrap_or(0); + + self.indexes + .prefix(prefix.as_slice()) + .skip(offset) + .take(limit) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = AuditLogValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt audit log entry"))?; + acc.push(self.value_to_audit_entry(&val)?); + Ok::<_, MetastoreError>(acc) + }) + } + + pub fn count_audit_log_entries(&self, delegated_did: &Did) -> Result { + let delegated_hash = UserHash::from_did(delegated_did.as_str()); + let prefix = audit_log_prefix(delegated_hash); + count_prefix(&self.indexes, prefix.as_slice()) + } + + fn value_to_audit_entry(&self, v: &AuditLogValue) -> Result { + Ok(AuditLogEntry { + id: v.id, + delegated_did: Did::new(v.delegated_did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?, + actor_did: Did::new(v.actor_did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid actor_did"))?, + controller_did: v + .controller_did + .as_ref() + .map(|d| Did::new(d.clone())) + .transpose() + .map_err(|_| MetastoreError::CorruptData("invalid controller_did"))?, + action_type: u8_to_action_type(v.action_type).ok_or(MetastoreError::CorruptData( + "unknown delegation action type", + ))?, + action_details: v + .action_details + .as_ref() + .and_then(|b| serde_json::from_slice(b).ok()), + ip_address: v.ip_address.clone(), + user_agent: v.user_agent.clone(), + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + }) + } +} diff --git a/crates/tranquil-store/src/metastore/delegations.rs b/crates/tranquil-store/src/metastore/delegations.rs new file mode 100644 index 0000000..8b55240 --- /dev/null +++ b/crates/tranquil-store/src/metastore/delegations.rs @@ -0,0 +1,201 @@ +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +use super::encoding::KeyBuilder; +use super::keys::{KeyTag, UserHash}; + +const GRANT_SCHEMA_VERSION: u8 = 1; +const AUDIT_LOG_SCHEMA_VERSION: u8 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DelegationGrantValue { + pub id: uuid::Uuid, + pub delegated_did: String, + pub controller_did: String, + pub granted_scopes: String, + pub granted_at_ms: i64, + pub granted_by: String, + pub revoked_at_ms: Option, + pub revoked_by: Option, +} + +impl DelegationGrantValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("DelegationGrantValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(GRANT_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + GRANT_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuditLogValue { + pub id: uuid::Uuid, + pub delegated_did: String, + pub actor_did: String, + pub controller_did: Option, + pub action_type: u8, + pub action_details: Option>, + pub ip_address: Option, + pub user_agent: Option, + pub created_at_ms: i64, +} + +impl AuditLogValue { + pub fn serialize(&self) -> Vec { + let payload = postcard::to_allocvec(self).expect("AuditLogValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(AUDIT_LOG_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + AUDIT_LOG_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +pub fn action_type_to_u8(t: tranquil_db_traits::DelegationActionType) -> u8 { + match t { + tranquil_db_traits::DelegationActionType::GrantCreated => 0, + tranquil_db_traits::DelegationActionType::GrantRevoked => 1, + tranquil_db_traits::DelegationActionType::ScopesModified => 2, + tranquil_db_traits::DelegationActionType::TokenIssued => 3, + tranquil_db_traits::DelegationActionType::RepoWrite => 4, + tranquil_db_traits::DelegationActionType::BlobUpload => 5, + tranquil_db_traits::DelegationActionType::AccountAction => 6, + } +} + +pub fn u8_to_action_type(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::DelegationActionType::GrantCreated), + 1 => Some(tranquil_db_traits::DelegationActionType::GrantRevoked), + 2 => Some(tranquil_db_traits::DelegationActionType::ScopesModified), + 3 => Some(tranquil_db_traits::DelegationActionType::TokenIssued), + 4 => Some(tranquil_db_traits::DelegationActionType::RepoWrite), + 5 => Some(tranquil_db_traits::DelegationActionType::BlobUpload), + 6 => Some(tranquil_db_traits::DelegationActionType::AccountAction), + _ => None, + } +} + +pub fn grant_key(delegated_hash: UserHash, controller_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::DELEG_GRANT) + .u64(delegated_hash.raw()) + .u64(controller_hash.raw()) + .build() +} + +pub fn grant_prefix(delegated_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::DELEG_GRANT) + .u64(delegated_hash.raw()) + .build() +} + +pub fn by_controller_key( + controller_hash: UserHash, + delegated_hash: UserHash, +) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::DELEG_BY_CONTROLLER) + .u64(controller_hash.raw()) + .u64(delegated_hash.raw()) + .build() +} + +pub fn by_controller_prefix(controller_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::DELEG_BY_CONTROLLER) + .u64(controller_hash.raw()) + .build() +} + +pub fn audit_log_key( + delegated_hash: UserHash, + created_at_ms: i64, + id: uuid::Uuid, +) -> SmallVec<[u8; 128]> { + let reversed_ts = i64::MAX.saturating_sub(created_at_ms); + KeyBuilder::new() + .tag(KeyTag::DELEG_AUDIT_LOG) + .u64(delegated_hash.raw()) + .i64(reversed_ts) + .bytes(id.as_bytes()) + .build() +} + +pub fn audit_log_prefix(delegated_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::DELEG_AUDIT_LOG) + .u64(delegated_hash.raw()) + .build() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grant_value_roundtrip() { + let val = DelegationGrantValue { + id: uuid::Uuid::new_v4(), + delegated_did: "did:plc:deleg".to_owned(), + controller_did: "did:plc:ctrl".to_owned(), + granted_scopes: "atproto".to_owned(), + granted_at_ms: 1700000000000, + granted_by: "did:plc:grantor".to_owned(), + revoked_at_ms: None, + revoked_by: None, + }; + let bytes = val.serialize(); + assert_eq!(bytes[0], GRANT_SCHEMA_VERSION); + let decoded = DelegationGrantValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn audit_log_value_roundtrip() { + let val = AuditLogValue { + id: uuid::Uuid::new_v4(), + delegated_did: "did:plc:deleg".to_owned(), + actor_did: "did:plc:actor".to_owned(), + controller_did: Some("did:plc:ctrl".to_owned()), + action_type: 0, + action_details: None, + ip_address: Some("127.0.0.1".to_owned()), + user_agent: None, + created_at_ms: 1700000000000, + }; + let bytes = val.serialize(); + assert_eq!(bytes[0], AUDIT_LOG_SCHEMA_VERSION); + let decoded = AuditLogValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn grant_key_ordering_by_controller() { + let deleg = UserHash::from_did("did:plc:deleg"); + let ctrl_a = UserHash::from_raw(100); + let ctrl_b = UserHash::from_raw(200); + let key_a = grant_key(deleg, ctrl_a); + let key_b = grant_key(deleg, ctrl_b); + assert!(key_a.as_slice() < key_b.as_slice()); + } +} diff --git a/crates/tranquil-store/src/metastore/handler.rs b/crates/tranquil-store/src/metastore/handler.rs index db15f0d..a5a7863 100644 --- a/crates/tranquil-store/src/metastore/handler.rs +++ b/crates/tranquil-store/src/metastore/handler.rs @@ -4,13 +4,34 @@ use std::thread::JoinHandle; use chrono::{DateTime, Utc}; use tokio::sync::oneshot; +use tranquil_db_traits::DbScope; use tranquil_db_traits::{ - AccountStatus, ApplyCommitError, ApplyCommitInput, ApplyCommitResult, Backlink, - BrokenGenesisCommit, CommitEventData, DbError, EventBlocksCids, ImportBlock, ImportRecord, - ImportRepoError, SequenceNumber, SequencedEvent, UserNeedingRecordBlobsBackfill, - UserWithoutBlocks, + AccountSearchResult, AccountStatus, AdminAccountInfo, ApplyCommitError, ApplyCommitInput, + ApplyCommitResult, Backlink, BrokenGenesisCommit, CommitEventData, CommsChannel, CommsType, + CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput, + CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult, + CreateSsoAccountInput, DbError, DelegationActionType, DeletionRequest, DidWebOverrides, + EventBlocksCids, ImportBlock, ImportRecord, ImportRepoError, InviteCodeError, InviteCodeInfo, + InviteCodeRow, InviteCodeSortOrder, InviteCodeUse, MigrationReactivationError, + MigrationReactivationInput, NotificationHistoryRow, NotificationPrefs, OAuthTokenWithUser, + PasswordResetResult, QueuedComms, ReactivatedAccountInfo, RecoverPasskeyAccountInput, + RecoverPasskeyAccountResult, RefreshSessionResult, ReservedSigningKey, + ScheduledDeletionAccount, ScopePreference, SequenceNumber, SequencedEvent, SessionId, + StoredBackupCode, StoredPasskey, TokenFamilyId, TotpRecord, TotpRecordState, User2faStatus, + UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, + UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, + UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle, + UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId, + UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, + UserNeedingRecordBlobsBackfill, UserPasswordInfo, UserResendVerification, UserResetCodeInfo, + UserRow, UserSessionInfo, UserStatus, UserVerificationInfo, UserWithKey, UserWithoutBlocks, + ValidatedInviteCode, WebauthnChallengeType, +}; +use tranquil_oauth::{AuthorizedClientData, DeviceData, RequestData, TokenData}; +use tranquil_types::{ + AtUri, AuthorizationCode, CidLink, ClientId, DPoPProofId, DeviceId, Did, Handle, Nsid, + RefreshToken, RequestId, Rkey, TokenId, }; -use tranquil_types::{AtUri, CidLink, Did, Handle, Nsid, Rkey}; use uuid::Uuid; use super::MetastoreError; @@ -77,6 +98,12 @@ pub enum MetastoreRequest { Commit(Box), Backlink(BacklinkRequest), Blob(BlobRequest), + Delegation(DelegationRequest), + Sso(SsoRequest), + Session(SessionRequest), + Infra(InfraRequest), + OAuth(OAuthRequest), + User(UserRequest), } impl MetastoreRequest { @@ -89,6 +116,12 @@ impl MetastoreRequest { Self::Commit(r) => r.routing(user_hashes), Self::Backlink(r) => r.routing(user_hashes), Self::Blob(r) => r.routing(user_hashes), + Self::Delegation(r) => r.routing(), + Self::Sso(r) => r.routing(), + Self::Session(r) => r.routing(user_hashes), + Self::Infra(r) => r.routing(user_hashes), + Self::OAuth(r) => r.routing(), + Self::User(r) => r.routing(user_hashes), } } } @@ -580,6 +613,1740 @@ impl BlobRequest { } } +pub enum DelegationRequest { + IsDelegatedAccount { + did: Did, + tx: Tx, + }, + CreateDelegation { + delegated_did: Did, + controller_did: Did, + granted_scopes: DbScope, + granted_by: Did, + tx: Tx, + }, + RevokeDelegation { + delegated_did: Did, + controller_did: Did, + revoked_by: Did, + tx: Tx, + }, + UpdateDelegationScopes { + delegated_did: Did, + controller_did: Did, + new_scopes: DbScope, + tx: Tx, + }, + GetDelegation { + delegated_did: Did, + controller_did: Did, + tx: Tx>, + }, + GetDelegationsForAccount { + delegated_did: Did, + tx: Tx>, + }, + GetAccountsControlledBy { + controller_did: Did, + tx: Tx>, + }, + CountActiveControllers { + delegated_did: Did, + tx: Tx, + }, + ControlsAnyAccounts { + did: Did, + tx: Tx, + }, + LogDelegationAction { + delegated_did: Did, + actor_did: Did, + controller_did: Option, + action_type: DelegationActionType, + action_details: Option, + ip_address: Option, + user_agent: Option, + tx: Tx, + }, + GetAuditLogForAccount { + delegated_did: Did, + limit: i64, + offset: i64, + tx: Tx>, + }, + CountAuditLogEntries { + delegated_did: Did, + tx: Tx, + }, +} + +impl DelegationRequest { + fn routing(&self) -> Routing { + match self { + Self::IsDelegatedAccount { did, .. } + | Self::GetDelegationsForAccount { + delegated_did: did, .. + } + | Self::CountActiveControllers { + delegated_did: did, .. + } + | Self::GetAuditLogForAccount { + delegated_did: did, .. + } + | Self::CountAuditLogEntries { + delegated_did: did, .. + } => did_to_routing(did.as_str()), + Self::CreateDelegation { delegated_did, .. } + | Self::RevokeDelegation { delegated_did, .. } + | Self::UpdateDelegationScopes { delegated_did, .. } + | Self::GetDelegation { delegated_did, .. } + | Self::LogDelegationAction { delegated_did, .. } => { + did_to_routing(delegated_did.as_str()) + } + Self::GetAccountsControlledBy { controller_did, .. } + | Self::ControlsAnyAccounts { + did: controller_did, + .. + } => did_to_routing(controller_did.as_str()), + } + } +} + +pub enum SsoRequest { + CreateExternalIdentity { + did: Did, + provider: tranquil_db_traits::SsoProviderType, + provider_user_id: String, + provider_username: Option, + provider_email: Option, + tx: Tx, + }, + GetExternalIdentityByProvider { + provider: tranquil_db_traits::SsoProviderType, + provider_user_id: String, + tx: Tx>, + }, + GetExternalIdentitiesByDid { + did: Did, + tx: Tx>, + }, + UpdateExternalIdentityLogin { + id: Uuid, + provider_username: Option, + provider_email: Option, + tx: Tx<()>, + }, + DeleteExternalIdentity { + id: Uuid, + did: Did, + tx: Tx, + }, + CreateSsoAuthState { + state: String, + request_uri: String, + provider: tranquil_db_traits::SsoProviderType, + action: tranquil_db_traits::SsoAction, + nonce: Option, + code_verifier: Option, + did: Option, + tx: Tx<()>, + }, + ConsumeSsoAuthState { + state: String, + tx: Tx>, + }, + CleanupExpiredSsoAuthStates { + tx: Tx, + }, + CreatePendingRegistration { + token: String, + request_uri: String, + provider: tranquil_db_traits::SsoProviderType, + provider_user_id: String, + provider_username: Option, + provider_email: Option, + provider_email_verified: bool, + tx: Tx<()>, + }, + GetPendingRegistration { + token: String, + tx: Tx>, + }, + ConsumePendingRegistration { + token: String, + tx: Tx>, + }, + CleanupExpiredPendingRegistrations { + tx: Tx, + }, +} + +impl SsoRequest { + fn routing(&self) -> Routing { + match self { + Self::CreateExternalIdentity { did, .. } + | Self::GetExternalIdentitiesByDid { did, .. } + | Self::DeleteExternalIdentity { did, .. } => did_to_routing(did.as_str()), + Self::GetExternalIdentityByProvider { .. } + | Self::UpdateExternalIdentityLogin { .. } + | Self::ConsumeSsoAuthState { .. } + | Self::CreateSsoAuthState { .. } + | Self::CleanupExpiredSsoAuthStates { .. } + | Self::CreatePendingRegistration { .. } + | Self::GetPendingRegistration { .. } + | Self::ConsumePendingRegistration { .. } + | Self::CleanupExpiredPendingRegistrations { .. } => Routing::Global, + } + } +} + +pub enum SessionRequest { + CreateSession { + data: tranquil_db_traits::SessionTokenCreate, + tx: Tx, + }, + GetSessionByAccessJti { + access_jti: String, + tx: Tx>, + }, + GetSessionForRefresh { + refresh_jti: String, + tx: Tx>, + }, + UpdateSessionTokens { + session_id: SessionId, + new_access_jti: String, + new_refresh_jti: String, + new_access_expires_at: DateTime, + new_refresh_expires_at: DateTime, + tx: Tx<()>, + }, + DeleteSessionByAccessJti { + access_jti: String, + tx: Tx, + }, + DeleteSessionById { + session_id: SessionId, + tx: Tx, + }, + DeleteSessionsByDid { + did: Did, + tx: Tx, + }, + DeleteSessionsByDidExceptJti { + did: Did, + except_jti: String, + tx: Tx, + }, + ListSessionsByDid { + did: Did, + tx: Tx>, + }, + GetSessionAccessJtiById { + session_id: SessionId, + did: Did, + tx: Tx>, + }, + DeleteSessionsByAppPassword { + did: Did, + app_password_name: String, + tx: Tx, + }, + GetSessionJtisByAppPassword { + did: Did, + app_password_name: String, + tx: Tx>, + }, + CheckRefreshTokenUsed { + refresh_jti: String, + tx: Tx>, + }, + MarkRefreshTokenUsed { + refresh_jti: String, + session_id: SessionId, + tx: Tx, + }, + ListAppPasswords { + user_id: Uuid, + tx: Tx>, + }, + GetAppPasswordsForLogin { + user_id: Uuid, + tx: Tx>, + }, + GetAppPasswordByName { + user_id: Uuid, + name: String, + tx: Tx>, + }, + CreateAppPassword { + data: tranquil_db_traits::AppPasswordCreate, + tx: Tx, + }, + DeleteAppPassword { + user_id: Uuid, + name: String, + tx: Tx, + }, + DeleteAppPasswordsByController { + did: Did, + controller_did: Did, + tx: Tx, + }, + GetLastReauthAt { + did: Did, + tx: Tx>>, + }, + UpdateLastReauth { + did: Did, + tx: Tx>, + }, + GetSessionMfaStatus { + did: Did, + tx: Tx>, + }, + UpdateMfaVerified { + did: Did, + tx: Tx<()>, + }, + GetAppPasswordHashesByDid { + did: Did, + tx: Tx>, + }, + RefreshSessionAtomic { + data: tranquil_db_traits::SessionRefreshData, + tx: Tx, + }, +} + +impl SessionRequest { + fn routing(&self, user_hashes: &UserHashMap) -> Routing { + match self { + Self::CreateSession { .. } + | Self::GetSessionByAccessJti { .. } + | Self::GetSessionForRefresh { .. } + | Self::CheckRefreshTokenUsed { .. } + | Self::MarkRefreshTokenUsed { .. } + | Self::DeleteSessionByAccessJti { .. } + | Self::DeleteSessionById { .. } => Routing::Global, + Self::DeleteSessionsByDid { did, .. } + | Self::DeleteSessionsByDidExceptJti { did, .. } + | Self::ListSessionsByDid { did, .. } + | Self::GetSessionAccessJtiById { did, .. } + | Self::DeleteSessionsByAppPassword { did, .. } + | Self::GetSessionJtisByAppPassword { did, .. } + | Self::DeleteAppPasswordsByController { did, .. } + | Self::GetLastReauthAt { did, .. } + | Self::UpdateLastReauth { did, .. } + | Self::GetSessionMfaStatus { did, .. } + | Self::UpdateMfaVerified { did, .. } + | Self::GetAppPasswordHashesByDid { did, .. } => did_to_routing(did.as_str()), + Self::UpdateSessionTokens { .. } | Self::RefreshSessionAtomic { .. } => Routing::Global, + Self::ListAppPasswords { user_id, .. } + | Self::GetAppPasswordsForLogin { user_id, .. } + | Self::GetAppPasswordByName { user_id, .. } + | Self::CreateAppPassword { + data: tranquil_db_traits::AppPasswordCreate { user_id, .. }, + .. + } + | Self::DeleteAppPassword { user_id, .. } => uuid_to_routing(user_hashes, user_id), + } + } +} + +pub enum UserRequest { + GetByDid { + did: Did, + tx: Tx>, + }, + GetByHandle { + handle: Handle, + tx: Tx>, + }, + GetWithKeyByDid { + did: Did, + tx: Tx>, + }, + GetStatusByDid { + did: Did, + tx: Tx>, + }, + CountUsers { + tx: Tx, + }, + GetSessionAccessExpiry { + did: Did, + access_jti: String, + tx: Tx>>, + }, + GetOAuthTokenWithUser { + token_id: String, + tx: Tx>, + }, + GetUserInfoByDid { + did: Did, + tx: Tx>, + }, + GetAnyAdminUserId { + tx: Tx>, + }, + SetInvitesDisabled { + did: Did, + disabled: bool, + tx: Tx, + }, + SearchAccounts { + cursor_did: Option, + email_filter: Option, + handle_filter: Option, + limit: i64, + tx: Tx>, + }, + GetAuthInfoByDid { + did: Did, + tx: Tx>, + }, + GetByEmail { + email: String, + tx: Tx>, + }, + GetLoginCheckByHandleOrEmail { + identifier: String, + tx: Tx>, + }, + GetLoginInfoByHandleOrEmail { + identifier: String, + tx: Tx>, + }, + Get2faStatusByDid { + did: Did, + tx: Tx>, + }, + GetCommsPrefs { + user_id: Uuid, + tx: Tx>, + }, + GetIdByDid { + did: Did, + tx: Tx>, + }, + GetUserKeyById { + user_id: Uuid, + tx: Tx>, + }, + GetIdAndHandleByDid { + did: Did, + tx: Tx>, + }, + GetDidWebInfoByHandle { + handle: Handle, + tx: Tx>, + }, + GetDidWebOverrides { + user_id: Uuid, + tx: Tx>, + }, + GetHandleByDid { + did: Did, + tx: Tx>, + }, + IsAccountActiveByDid { + did: Did, + tx: Tx>, + }, + GetUserForDeletion { + did: Did, + tx: Tx>, + }, + CheckHandleExists { + handle: Handle, + exclude_user_id: Uuid, + tx: Tx, + }, + UpdateHandle { + user_id: Uuid, + handle: Handle, + tx: Tx<()>, + }, + GetUserWithKeyByDid { + did: Did, + tx: Tx>, + }, + IsAccountMigrated { + did: Did, + tx: Tx, + }, + HasVerifiedCommsChannel { + did: Did, + tx: Tx, + }, + GetIdByHandle { + handle: Handle, + tx: Tx>, + }, + GetEmailInfoByDid { + did: Did, + tx: Tx>, + }, + CheckEmailExists { + email: String, + exclude_user_id: Uuid, + tx: Tx, + }, + UpdateEmail { + user_id: Uuid, + email: String, + tx: Tx<()>, + }, + SetEmailVerified { + user_id: Uuid, + verified: bool, + tx: Tx<()>, + }, + CheckEmailVerifiedByIdentifier { + identifier: String, + tx: Tx>, + }, + CheckChannelVerifiedByDid { + did: Did, + channel: CommsChannel, + tx: Tx>, + }, + AdminUpdateEmail { + did: Did, + email: String, + tx: Tx, + }, + AdminUpdateHandle { + did: Did, + handle: Handle, + tx: Tx, + }, + AdminUpdatePassword { + did: Did, + password_hash: String, + tx: Tx, + }, + GetNotificationPrefs { + did: Did, + tx: Tx>, + }, + GetIdHandleEmailByDid { + did: Did, + tx: Tx>, + }, + UpdatePreferredCommsChannel { + did: Did, + channel: CommsChannel, + tx: Tx<()>, + }, + ClearDiscord { + user_id: Uuid, + tx: Tx<()>, + }, + ClearTelegram { + user_id: Uuid, + tx: Tx<()>, + }, + ClearSignal { + user_id: Uuid, + tx: Tx<()>, + }, + SetUnverifiedSignal { + user_id: Uuid, + signal_username: String, + tx: Tx<()>, + }, + SetUnverifiedTelegram { + user_id: Uuid, + telegram_username: String, + tx: Tx<()>, + }, + StoreTelegramChatId { + telegram_username: String, + chat_id: i64, + handle: Option, + tx: Tx>, + }, + GetTelegramChatId { + user_id: Uuid, + tx: Tx>, + }, + SetUnverifiedDiscord { + user_id: Uuid, + discord_username: String, + tx: Tx<()>, + }, + StoreDiscordUserId { + discord_username: String, + discord_id: String, + handle: Option, + tx: Tx>, + }, + GetVerificationInfo { + did: Did, + tx: Tx>, + }, + VerifyEmailChannel { + user_id: Uuid, + email: String, + tx: Tx, + }, + VerifyDiscordChannel { + user_id: Uuid, + discord_id: String, + tx: Tx<()>, + }, + VerifyTelegramChannel { + user_id: Uuid, + telegram_username: String, + tx: Tx<()>, + }, + VerifySignalChannel { + user_id: Uuid, + signal_username: String, + tx: Tx<()>, + }, + SetEmailVerifiedFlag { + user_id: Uuid, + tx: Tx<()>, + }, + SetDiscordVerifiedFlag { + user_id: Uuid, + tx: Tx<()>, + }, + SetTelegramVerifiedFlag { + user_id: Uuid, + tx: Tx<()>, + }, + SetSignalVerifiedFlag { + user_id: Uuid, + tx: Tx<()>, + }, + HasTotpEnabled { + did: Did, + tx: Tx, + }, + HasPasskeys { + did: Did, + tx: Tx, + }, + GetPasswordHashByDid { + did: Did, + tx: Tx>, + }, + GetPasskeysForUser { + did: Did, + tx: Tx>, + }, + GetPasskeyByCredentialId { + credential_id: Vec, + tx: Tx>, + }, + SavePasskey { + did: Did, + credential_id: Vec, + public_key: Vec, + friendly_name: Option, + tx: Tx, + }, + UpdatePasskeyCounter { + credential_id: Vec, + new_counter: i32, + tx: Tx, + }, + DeletePasskey { + id: Uuid, + did: Did, + tx: Tx, + }, + UpdatePasskeyName { + id: Uuid, + did: Did, + name: String, + tx: Tx, + }, + SaveWebauthnChallenge { + did: Did, + challenge_type: WebauthnChallengeType, + state_json: String, + tx: Tx, + }, + LoadWebauthnChallenge { + did: Did, + challenge_type: WebauthnChallengeType, + tx: Tx>, + }, + DeleteWebauthnChallenge { + did: Did, + challenge_type: WebauthnChallengeType, + tx: Tx<()>, + }, + GetTotpRecord { + did: Did, + tx: Tx>, + }, + GetTotpRecordState { + did: Did, + tx: Tx>, + }, + UpsertTotpSecret { + did: Did, + secret_encrypted: Vec, + encryption_version: i32, + tx: Tx<()>, + }, + SetTotpVerified { + did: Did, + tx: Tx<()>, + }, + UpdateTotpLastUsed { + did: Did, + tx: Tx<()>, + }, + DeleteTotp { + did: Did, + tx: Tx<()>, + }, + GetUnusedBackupCodes { + did: Did, + tx: Tx>, + }, + MarkBackupCodeUsed { + code_id: Uuid, + tx: Tx, + }, + CountUnusedBackupCodes { + did: Did, + tx: Tx, + }, + DeleteBackupCodes { + did: Did, + tx: Tx, + }, + InsertBackupCodes { + did: Did, + code_hashes: Vec, + tx: Tx<()>, + }, + EnableTotpWithBackupCodes { + did: Did, + code_hashes: Vec, + tx: Tx<()>, + }, + DeleteTotpAndBackupCodes { + did: Did, + tx: Tx<()>, + }, + ReplaceBackupCodes { + did: Did, + code_hashes: Vec, + tx: Tx<()>, + }, + GetSessionInfoByDid { + did: Did, + tx: Tx>, + }, + GetLegacyLoginPref { + did: Did, + tx: Tx>, + }, + UpdateLegacyLogin { + did: Did, + allow: bool, + tx: Tx, + }, + UpdateLocale { + did: Did, + locale: String, + tx: Tx, + }, + GetLoginFullByIdentifier { + identifier: String, + tx: Tx>, + }, + GetConfirmSignupByDid { + did: Did, + tx: Tx>, + }, + GetResendVerificationByDid { + did: Did, + tx: Tx>, + }, + SetChannelVerified { + did: Did, + channel: CommsChannel, + tx: Tx<()>, + }, + GetIdByEmailOrHandle { + email: String, + handle: String, + tx: Tx>, + }, + CountAccountsByEmail { + email: String, + tx: Tx, + }, + GetHandlesByEmail { + email: String, + tx: Tx>, + }, + SetPasswordResetCode { + user_id: Uuid, + code: String, + expires_at: DateTime, + tx: Tx<()>, + }, + GetUserByResetCode { + code: String, + tx: Tx>, + }, + ClearPasswordResetCode { + user_id: Uuid, + tx: Tx<()>, + }, + GetIdAndPasswordHashByDid { + did: Did, + tx: Tx>, + }, + UpdatePasswordHash { + user_id: Uuid, + password_hash: String, + tx: Tx<()>, + }, + ResetPasswordWithSessions { + user_id: Uuid, + password_hash: String, + tx: Tx, + }, + ActivateAccount { + did: Did, + tx: Tx, + }, + DeactivateAccount { + did: Did, + delete_after: Option>, + tx: Tx, + }, + HasPasswordByDid { + did: Did, + tx: Tx>, + }, + GetPasswordInfoByDid { + did: Did, + tx: Tx>, + }, + RemoveUserPassword { + user_id: Uuid, + tx: Tx<()>, + }, + SetNewUserPassword { + user_id: Uuid, + password_hash: String, + tx: Tx<()>, + }, + GetUserKeyByDid { + did: Did, + tx: Tx>, + }, + DeleteAccountComplete { + user_id: Uuid, + did: Did, + tx: Tx<()>, + }, + SetUserTakedown { + did: Did, + takedown_ref: Option, + tx: Tx, + }, + AdminDeleteAccountComplete { + user_id: Uuid, + did: Did, + tx: Tx<()>, + }, + GetUserForDidDoc { + did: Did, + tx: Tx>, + }, + GetUserForDidDocBuild { + did: Did, + tx: Tx>, + }, + UpsertDidWebOverrides { + user_id: Uuid, + verification_methods: Option, + also_known_as: Option>, + tx: Tx<()>, + }, + UpdateMigratedToPds { + did: Did, + endpoint: String, + tx: Tx<()>, + }, + GetUserForPasskeySetup { + did: Did, + tx: Tx>, + }, + GetUserForPasskeyRecovery { + identifier: String, + normalized_handle: String, + tx: Tx>, + }, + SetRecoveryToken { + did: Did, + token_hash: String, + expires_at: DateTime, + tx: Tx<()>, + }, + GetUserForRecovery { + did: Did, + tx: Tx>, + }, + GetAccountsScheduledForDeletion { + limit: i64, + tx: Tx>, + }, + DeleteAccountWithFirehose { + user_id: Uuid, + did: Did, + tx: Tx, + }, + CreatePasswordAccount { + input: CreatePasswordAccountInput, + tx: oneshot::Sender>, + }, + CreateDelegatedAccount { + input: CreateDelegatedAccountInput, + tx: oneshot::Sender>, + }, + CreatePasskeyAccount { + input: CreatePasskeyAccountInput, + tx: oneshot::Sender>, + }, + CreateSsoAccount { + input: CreateSsoAccountInput, + tx: oneshot::Sender>, + }, + ReactivateMigrationAccount { + input: MigrationReactivationInput, + tx: oneshot::Sender>, + }, + CheckHandleAvailableForNewAccount { + handle: Handle, + tx: Tx, + }, + ReserveHandle { + handle: Handle, + reserved_by: String, + tx: Tx, + }, + ReleaseHandleReservation { + handle: Handle, + tx: Tx<()>, + }, + CleanupExpiredHandleReservations { + tx: Tx, + }, + CheckAndConsumeInviteCode { + code: String, + tx: Tx, + }, + CompletePasskeySetup { + input: CompletePasskeySetupInput, + tx: Tx<()>, + }, + RecoverPasskeyAccount { + input: RecoverPasskeyAccountInput, + tx: Tx, + }, +} + +impl UserRequest { + fn routing(&self, user_hashes: &UserHashMap) -> Routing { + match self { + Self::GetByDid { did, .. } + | Self::GetWithKeyByDid { did, .. } + | Self::GetStatusByDid { did, .. } + | Self::GetSessionAccessExpiry { did, .. } + | Self::GetUserInfoByDid { did, .. } + | Self::SetInvitesDisabled { did, .. } + | Self::GetAuthInfoByDid { did, .. } + | Self::Get2faStatusByDid { did, .. } + | Self::GetIdByDid { did, .. } + | Self::GetIdAndHandleByDid { did, .. } + | Self::GetHandleByDid { did, .. } + | Self::IsAccountActiveByDid { did, .. } + | Self::GetUserForDeletion { did, .. } + | Self::GetUserWithKeyByDid { did, .. } + | Self::IsAccountMigrated { did, .. } + | Self::HasVerifiedCommsChannel { did, .. } + | Self::GetEmailInfoByDid { did, .. } + | Self::CheckChannelVerifiedByDid { did, .. } + | Self::AdminUpdateEmail { did, .. } + | Self::AdminUpdateHandle { did, .. } + | Self::AdminUpdatePassword { did, .. } + | Self::GetNotificationPrefs { did, .. } + | Self::GetIdHandleEmailByDid { did, .. } + | Self::UpdatePreferredCommsChannel { did, .. } + | Self::GetVerificationInfo { did, .. } + | Self::HasTotpEnabled { did, .. } + | Self::HasPasskeys { did, .. } + | Self::GetPasswordHashByDid { did, .. } + | Self::GetPasskeysForUser { did, .. } + | Self::SavePasskey { did, .. } + | Self::DeletePasskey { did, .. } + | Self::UpdatePasskeyName { did, .. } + | Self::SaveWebauthnChallenge { did, .. } + | Self::LoadWebauthnChallenge { did, .. } + | Self::DeleteWebauthnChallenge { did, .. } + | Self::GetTotpRecord { did, .. } + | Self::GetTotpRecordState { did, .. } + | Self::UpsertTotpSecret { did, .. } + | Self::SetTotpVerified { did, .. } + | Self::UpdateTotpLastUsed { did, .. } + | Self::DeleteTotp { did, .. } + | Self::GetUnusedBackupCodes { did, .. } + | Self::CountUnusedBackupCodes { did, .. } + | Self::DeleteBackupCodes { did, .. } + | Self::InsertBackupCodes { did, .. } + | Self::EnableTotpWithBackupCodes { did, .. } + | Self::DeleteTotpAndBackupCodes { did, .. } + | Self::ReplaceBackupCodes { did, .. } + | Self::GetSessionInfoByDid { did, .. } + | Self::GetLegacyLoginPref { did, .. } + | Self::UpdateLegacyLogin { did, .. } + | Self::UpdateLocale { did, .. } + | Self::GetConfirmSignupByDid { did, .. } + | Self::GetResendVerificationByDid { did, .. } + | Self::SetChannelVerified { did, .. } + | Self::GetIdAndPasswordHashByDid { did, .. } + | Self::ActivateAccount { did, .. } + | Self::DeactivateAccount { did, .. } + | Self::HasPasswordByDid { did, .. } + | Self::GetPasswordInfoByDid { did, .. } + | Self::GetUserKeyByDid { did, .. } + | Self::DeleteAccountComplete { did, .. } + | Self::SetUserTakedown { did, .. } + | Self::AdminDeleteAccountComplete { did, .. } + | Self::GetUserForDidDoc { did, .. } + | Self::GetUserForDidDocBuild { did, .. } + | Self::UpdateMigratedToPds { did, .. } + | Self::GetUserForPasskeySetup { did, .. } + | Self::GetUserForRecovery { did, .. } + | Self::DeleteAccountWithFirehose { did, .. } + | Self::CreatePasswordAccount { + input: CreatePasswordAccountInput { did, .. }, + .. + } + | Self::CreateDelegatedAccount { + input: CreateDelegatedAccountInput { did, .. }, + .. + } + | Self::CreatePasskeyAccount { + input: CreatePasskeyAccountInput { did, .. }, + .. + } + | Self::CreateSsoAccount { + input: CreateSsoAccountInput { did, .. }, + .. + } + | Self::ReactivateMigrationAccount { + input: MigrationReactivationInput { did, .. }, + .. + } + | Self::CompletePasskeySetup { + input: CompletePasskeySetupInput { did, .. }, + .. + } + | Self::RecoverPasskeyAccount { + input: RecoverPasskeyAccountInput { did, .. }, + .. + } + | Self::SetRecoveryToken { did, .. } => did_to_routing(did.as_str()), + + Self::GetCommsPrefs { user_id, .. } + | Self::GetUserKeyById { user_id, .. } + | Self::GetDidWebOverrides { user_id, .. } + | Self::UpdateHandle { user_id, .. } + | Self::UpdateEmail { user_id, .. } + | Self::SetEmailVerified { user_id, .. } + | Self::ClearDiscord { user_id, .. } + | Self::ClearTelegram { user_id, .. } + | Self::ClearSignal { user_id, .. } + | Self::SetUnverifiedSignal { user_id, .. } + | Self::SetUnverifiedTelegram { user_id, .. } + | Self::GetTelegramChatId { user_id, .. } + | Self::SetUnverifiedDiscord { user_id, .. } + | Self::VerifyEmailChannel { user_id, .. } + | Self::VerifyDiscordChannel { user_id, .. } + | Self::VerifyTelegramChannel { user_id, .. } + | Self::VerifySignalChannel { user_id, .. } + | Self::SetEmailVerifiedFlag { user_id, .. } + | Self::SetDiscordVerifiedFlag { user_id, .. } + | Self::SetTelegramVerifiedFlag { user_id, .. } + | Self::SetSignalVerifiedFlag { user_id, .. } + | Self::SetPasswordResetCode { user_id, .. } + | Self::ClearPasswordResetCode { user_id, .. } + | Self::UpdatePasswordHash { user_id, .. } + | Self::ResetPasswordWithSessions { user_id, .. } + | Self::RemoveUserPassword { user_id, .. } + | Self::SetNewUserPassword { user_id, .. } + | Self::UpsertDidWebOverrides { user_id, .. } => uuid_to_routing(user_hashes, user_id), + + Self::CheckHandleExists { + exclude_user_id, .. + } + | Self::CheckEmailExists { + exclude_user_id, .. + } => uuid_to_routing(user_hashes, exclude_user_id), + + Self::MarkBackupCodeUsed { .. } => Routing::Global, + + Self::GetByHandle { .. } + | Self::GetDidWebInfoByHandle { .. } + | Self::GetIdByHandle { .. } + | Self::CheckHandleAvailableForNewAccount { .. } + | Self::ReserveHandle { .. } + | Self::ReleaseHandleReservation { .. } => Routing::Global, + + Self::CountUsers { .. } + | Self::GetOAuthTokenWithUser { .. } + | Self::GetAnyAdminUserId { .. } + | Self::SearchAccounts { .. } + | Self::GetByEmail { .. } + | Self::GetLoginCheckByHandleOrEmail { .. } + | Self::GetLoginInfoByHandleOrEmail { .. } + | Self::CheckEmailVerifiedByIdentifier { .. } + | Self::StoreTelegramChatId { .. } + | Self::StoreDiscordUserId { .. } + | Self::GetPasskeyByCredentialId { .. } + | Self::UpdatePasskeyCounter { .. } + | Self::GetLoginFullByIdentifier { .. } + | Self::GetIdByEmailOrHandle { .. } + | Self::CountAccountsByEmail { .. } + | Self::GetHandlesByEmail { .. } + | Self::GetUserByResetCode { .. } + | Self::GetUserForPasskeyRecovery { .. } + | Self::GetAccountsScheduledForDeletion { .. } + | Self::CleanupExpiredHandleReservations { .. } + | Self::CheckAndConsumeInviteCode { .. } => Routing::Global, + } + } +} + +pub enum InfraRequest { + EnqueueComms { + user_id: Option, + channel: CommsChannel, + comms_type: CommsType, + recipient: String, + subject: Option, + body: String, + metadata: Option, + tx: Tx, + }, + FetchPendingComms { + now: DateTime, + batch_size: i64, + tx: Tx>, + }, + MarkCommsSent { + id: Uuid, + tx: Tx<()>, + }, + MarkCommsFailed { + id: Uuid, + error: String, + tx: Tx<()>, + }, + CreateInviteCode { + code: String, + use_count: i32, + for_account: Option, + tx: Tx, + }, + CreateInviteCodesBatch { + codes: Vec, + use_count: i32, + created_by_user: Uuid, + for_account: Option, + tx: Tx<()>, + }, + GetInviteCodeAvailableUses { + code: String, + tx: Tx>, + }, + ValidateInviteCode { + code: String, + tx: oneshot::Sender>, + }, + DecrementInviteCodeUses { + code: String, + tx: Tx<()>, + }, + RecordInviteCodeUse { + code: String, + used_by_user: Uuid, + tx: Tx<()>, + }, + GetInviteCodesForAccount { + for_account: Did, + tx: Tx>, + }, + GetInviteCodeUses { + code: String, + tx: Tx>, + }, + DisableInviteCodesByCode { + codes: Vec, + tx: Tx<()>, + }, + DisableInviteCodesByAccount { + accounts: Vec, + tx: Tx<()>, + }, + ListInviteCodes { + cursor: Option, + limit: i64, + sort: InviteCodeSortOrder, + tx: Tx>, + }, + GetUserDidsByIds { + user_ids: Vec, + tx: Tx>, + }, + GetInviteCodeUsesBatch { + codes: Vec, + tx: Tx>, + }, + GetInvitesCreatedByUser { + user_id: Uuid, + tx: Tx>, + }, + GetInviteCodeInfo { + code: String, + tx: Tx>, + }, + GetInviteCodesByUsers { + user_ids: Vec, + tx: Tx>, + }, + GetInviteCodeUsedByUser { + user_id: Uuid, + tx: Tx>, + }, + DeleteInviteCodeUsesByUser { + user_id: Uuid, + tx: Tx<()>, + }, + DeleteInviteCodesByUser { + user_id: Uuid, + tx: Tx<()>, + }, + ReserveSigningKey { + did: Option, + public_key_did_key: String, + private_key_bytes: Vec, + expires_at: DateTime, + tx: Tx, + }, + GetReservedSigningKey { + public_key_did_key: String, + tx: Tx>, + }, + MarkSigningKeyUsed { + key_id: Uuid, + tx: Tx<()>, + }, + CreateDeletionRequest { + token: String, + did: Did, + expires_at: DateTime, + tx: Tx<()>, + }, + GetDeletionRequest { + token: String, + tx: Tx>, + }, + DeleteDeletionRequest { + token: String, + tx: Tx<()>, + }, + DeleteDeletionRequestsByDid { + did: Did, + tx: Tx<()>, + }, + UpsertAccountPreference { + user_id: Uuid, + name: String, + value_json: serde_json::Value, + tx: Tx<()>, + }, + InsertAccountPreferenceIfNotExists { + user_id: Uuid, + name: String, + value_json: serde_json::Value, + tx: Tx<()>, + }, + GetServerConfig { + key: String, + tx: Tx>, + }, + InsertReport { + id: i64, + reason_type: String, + reason: Option, + subject_json: serde_json::Value, + reported_by_did: Did, + created_at: DateTime, + tx: Tx<()>, + }, + DeletePlcTokensForUser { + user_id: Uuid, + tx: Tx<()>, + }, + InsertPlcToken { + user_id: Uuid, + token: String, + expires_at: DateTime, + tx: Tx<()>, + }, + GetPlcTokenExpiry { + user_id: Uuid, + token: String, + tx: Tx>>, + }, + DeletePlcToken { + user_id: Uuid, + token: String, + tx: Tx<()>, + }, + GetAccountPreferences { + user_id: Uuid, + tx: Tx>, + }, + ReplaceNamespacePreferences { + user_id: Uuid, + namespace: String, + preferences: Vec<(String, serde_json::Value)>, + tx: Tx<()>, + }, + GetNotificationHistory { + user_id: Uuid, + limit: i64, + tx: Tx>, + }, + GetServerConfigs { + keys: Vec, + tx: Tx>, + }, + UpsertServerConfig { + key: String, + value: String, + tx: Tx<()>, + }, + DeleteServerConfig { + key: String, + tx: Tx<()>, + }, + GetBlobStorageKeyByCid { + cid: CidLink, + tx: Tx>, + }, + DeleteBlobByCid { + cid: CidLink, + tx: Tx<()>, + }, + GetAdminAccountInfoByDid { + did: Did, + tx: Tx>, + }, + GetAdminAccountInfosByDids { + dids: Vec, + tx: Tx>, + }, + GetInviteCodeUsesByUsers { + user_ids: Vec, + tx: Tx>, + }, +} + +impl InfraRequest { + fn routing(&self, user_hashes: &UserHashMap) -> Routing { + match self { + Self::UpsertAccountPreference { user_id, .. } + | Self::InsertAccountPreferenceIfNotExists { user_id, .. } + | Self::GetAccountPreferences { user_id, .. } + | Self::ReplaceNamespacePreferences { user_id, .. } + | Self::GetNotificationHistory { user_id, .. } + | Self::DeletePlcTokensForUser { user_id, .. } + | Self::InsertPlcToken { user_id, .. } + | Self::GetPlcTokenExpiry { user_id, .. } + | Self::DeletePlcToken { user_id, .. } + | Self::GetInvitesCreatedByUser { user_id, .. } + | Self::GetInviteCodeUsedByUser { user_id, .. } + | Self::DeleteInviteCodeUsesByUser { user_id, .. } + | Self::DeleteInviteCodesByUser { user_id, .. } => { + uuid_to_routing(user_hashes, user_id) + } + Self::CreateInviteCodesBatch { + created_by_user, .. + } => uuid_to_routing(user_hashes, created_by_user), + Self::RecordInviteCodeUse { used_by_user, .. } => { + uuid_to_routing(user_hashes, used_by_user) + } + Self::EnqueueComms { + user_id: Some(uid), .. + } => uuid_to_routing(user_hashes, uid), + Self::GetInviteCodesForAccount { for_account, .. } => { + did_to_routing(for_account.as_str()) + } + Self::DeleteDeletionRequestsByDid { did, .. } + | Self::CreateDeletionRequest { did, .. } + | Self::GetAdminAccountInfoByDid { did, .. } => did_to_routing(did.as_str()), + Self::GetBlobStorageKeyByCid { cid, .. } | Self::DeleteBlobByCid { cid, .. } => { + cid_to_routing(cid) + } + _ => Routing::Global, + } + } +} + +pub enum OAuthRequest { + CreateToken { + data: TokenData, + tx: Tx, + }, + GetTokenById { + token_id: TokenId, + tx: Tx>, + }, + GetTokenByRefreshToken { + refresh_token: RefreshToken, + tx: Tx>, + }, + GetTokenByPreviousRefreshToken { + refresh_token: RefreshToken, + tx: Tx>, + }, + RotateToken { + old_db_id: TokenFamilyId, + new_refresh_token: RefreshToken, + new_expires_at: DateTime, + tx: Tx<()>, + }, + CheckRefreshTokenUsed { + refresh_token: RefreshToken, + tx: Tx>, + }, + DeleteToken { + token_id: TokenId, + tx: Tx<()>, + }, + DeleteTokenFamily { + db_id: TokenFamilyId, + tx: Tx<()>, + }, + ListTokensForUser { + did: Did, + tx: Tx>, + }, + CountTokensForUser { + did: Did, + tx: Tx, + }, + DeleteOldestTokensForUser { + did: Did, + keep_count: i64, + tx: Tx, + }, + RevokeTokensForClient { + did: Did, + client_id: ClientId, + tx: Tx, + }, + RevokeTokensForController { + delegated_did: Did, + controller_did: Did, + tx: Tx, + }, + CreateAuthorizationRequest { + request_id: RequestId, + data: RequestData, + tx: Tx<()>, + }, + GetAuthorizationRequest { + request_id: RequestId, + tx: Tx>, + }, + SetAuthorizationDid { + request_id: RequestId, + did: Did, + device_id: Option, + tx: Tx<()>, + }, + UpdateAuthorizationRequest { + request_id: RequestId, + did: Did, + device_id: Option, + code: AuthorizationCode, + tx: Tx<()>, + }, + ConsumeAuthorizationRequestByCode { + code: AuthorizationCode, + tx: Tx>, + }, + DeleteAuthorizationRequest { + request_id: RequestId, + tx: Tx<()>, + }, + DeleteExpiredAuthorizationRequests { + tx: Tx, + }, + ExtendAuthorizationRequestExpiry { + request_id: RequestId, + new_expires_at: DateTime, + tx: Tx, + }, + MarkRequestAuthenticated { + request_id: RequestId, + did: Did, + device_id: Option, + tx: Tx<()>, + }, + UpdateRequestScope { + request_id: RequestId, + scope: String, + tx: Tx<()>, + }, + SetControllerDid { + request_id: RequestId, + controller_did: Did, + tx: Tx<()>, + }, + SetRequestDid { + request_id: RequestId, + did: Did, + tx: Tx<()>, + }, + CreateDevice { + device_id: DeviceId, + data: DeviceData, + tx: Tx<()>, + }, + GetDevice { + device_id: DeviceId, + tx: Tx>, + }, + UpdateDeviceLastSeen { + device_id: DeviceId, + tx: Tx<()>, + }, + DeleteDevice { + device_id: DeviceId, + tx: Tx<()>, + }, + UpsertAccountDevice { + did: Did, + device_id: DeviceId, + tx: Tx<()>, + }, + GetDeviceAccounts { + device_id: DeviceId, + tx: Tx>, + }, + VerifyAccountOnDevice { + device_id: DeviceId, + did: Did, + tx: Tx, + }, + CheckAndRecordDpopJti { + jti: DPoPProofId, + tx: Tx, + }, + CleanupExpiredDpopJtis { + max_age_secs: i64, + tx: Tx, + }, + Create2faChallenge { + did: Did, + request_uri: RequestId, + tx: Tx, + }, + Get2faChallenge { + request_uri: RequestId, + tx: Tx>, + }, + Increment2faAttempts { + id: Uuid, + tx: Tx, + }, + Delete2faChallenge { + id: Uuid, + tx: Tx<()>, + }, + Delete2faChallengeByRequestUri { + request_uri: RequestId, + tx: Tx<()>, + }, + CleanupExpired2faChallenges { + tx: Tx, + }, + CheckUser2faEnabled { + did: Did, + tx: Tx, + }, + GetScopePreferences { + did: Did, + client_id: ClientId, + tx: Tx>, + }, + UpsertScopePreferences { + did: Did, + client_id: ClientId, + prefs: Vec, + tx: Tx<()>, + }, + DeleteScopePreferences { + did: Did, + client_id: ClientId, + tx: Tx<()>, + }, + UpsertAuthorizedClient { + did: Did, + client_id: ClientId, + data: AuthorizedClientData, + tx: Tx<()>, + }, + GetAuthorizedClient { + did: Did, + client_id: ClientId, + tx: Tx>, + }, + ListTrustedDevices { + did: Did, + tx: Tx>, + }, + GetDeviceTrustInfo { + device_id: DeviceId, + did: Did, + tx: Tx>, + }, + DeviceBelongsToUser { + device_id: DeviceId, + did: Did, + tx: Tx, + }, + RevokeDeviceTrust { + device_id: DeviceId, + did: Did, + tx: Tx<()>, + }, + UpdateDeviceFriendlyName { + device_id: DeviceId, + did: Did, + friendly_name: Option, + tx: Tx<()>, + }, + TrustDevice { + device_id: DeviceId, + did: Did, + trusted_at: DateTime, + trusted_until: DateTime, + tx: Tx<()>, + }, + ExtendDeviceTrust { + device_id: DeviceId, + did: Did, + trusted_until: DateTime, + tx: Tx<()>, + }, + ListSessionsByDid { + did: Did, + tx: Tx>, + }, + DeleteSessionById { + session_id: TokenFamilyId, + did: Did, + tx: Tx, + }, + DeleteSessionsByDid { + did: Did, + tx: Tx, + }, + DeleteSessionsByDidExcept { + did: Did, + except_token_id: TokenId, + tx: Tx, + }, +} + +impl OAuthRequest { + fn routing(&self) -> Routing { + match self { + Self::ListTokensForUser { did, .. } + | Self::CountTokensForUser { did, .. } + | Self::DeleteOldestTokensForUser { did, .. } + | Self::RevokeTokensForClient { did, .. } + | Self::Create2faChallenge { did, .. } + | Self::CheckUser2faEnabled { did, .. } + | Self::GetScopePreferences { did, .. } + | Self::UpsertScopePreferences { did, .. } + | Self::DeleteScopePreferences { did, .. } + | Self::UpsertAuthorizedClient { did, .. } + | Self::GetAuthorizedClient { did, .. } + | Self::ListTrustedDevices { did, .. } + | Self::GetDeviceTrustInfo { did, .. } + | Self::DeviceBelongsToUser { did, .. } + | Self::UpsertAccountDevice { did, .. } + | Self::VerifyAccountOnDevice { did, .. } + | Self::ListSessionsByDid { did, .. } + | Self::DeleteSessionsByDid { did, .. } + | Self::DeleteSessionsByDidExcept { did, .. } + | Self::DeleteSessionById { did, .. } => did_to_routing(did.as_str()), + Self::RevokeTokensForController { delegated_did, .. } => { + did_to_routing(delegated_did.as_str()) + } + Self::SetAuthorizationDid { did, .. } + | Self::UpdateAuthorizationRequest { did, .. } + | Self::MarkRequestAuthenticated { did, .. } + | Self::SetRequestDid { did, .. } => did_to_routing(did.as_str()), + Self::CreateToken { .. } + | Self::GetTokenById { .. } + | Self::GetTokenByRefreshToken { .. } + | Self::GetTokenByPreviousRefreshToken { .. } + | Self::RotateToken { .. } + | Self::CheckRefreshTokenUsed { .. } + | Self::DeleteToken { .. } + | Self::DeleteTokenFamily { .. } + | Self::CreateAuthorizationRequest { .. } + | Self::GetAuthorizationRequest { .. } + | Self::ConsumeAuthorizationRequestByCode { .. } + | Self::DeleteAuthorizationRequest { .. } + | Self::DeleteExpiredAuthorizationRequests { .. } + | Self::ExtendAuthorizationRequestExpiry { .. } + | Self::UpdateRequestScope { .. } + | Self::SetControllerDid { .. } + | Self::CreateDevice { .. } + | Self::GetDevice { .. } + | Self::UpdateDeviceLastSeen { .. } + | Self::DeleteDevice { .. } + | Self::GetDeviceAccounts { .. } + | Self::CheckAndRecordDpopJti { .. } + | Self::CleanupExpiredDpopJtis { .. } + | Self::Get2faChallenge { .. } + | Self::Increment2faAttempts { .. } + | Self::Delete2faChallenge { .. } + | Self::Delete2faChallengeByRequestUri { .. } + | Self::CleanupExpired2faChallenges { .. } + | Self::RevokeDeviceTrust { .. } + | Self::UpdateDeviceFriendlyName { .. } + | Self::TrustDevice { .. } + | Self::ExtendDeviceTrust { .. } => Routing::Global, + } + } +} + fn convert_repo_info(r: super::repo_ops::RepoInfo) -> tranquil_db_traits::RepoInfo { tranquil_db_traits::RepoInfo { user_id: r.user_id, @@ -928,7 +2695,7 @@ fn dispatch_record(state: &HandlerState, req: RecordRequest) { user_id: repo_id, collection: &collection, cursor: cursor.as_ref(), - limit: usize::try_from(limit).unwrap_or(usize::MAX), + limit: usize::try_from(limit).unwrap_or(0), reverse, rkey_start: rkey_start.as_ref(), rkey_end: rkey_end.as_ref(), @@ -1348,7 +3115,7 @@ fn dispatch_blob(state: &HandlerState, req: BlobRequest) { .list_blobs_by_user( user_id, cursor.as_deref(), - usize::try_from(limit).unwrap_or(usize::MAX), + usize::try_from(limit).unwrap_or(0), ) .map_err(metastore_to_db); let _ = tx.send(result); @@ -1421,7 +3188,7 @@ fn dispatch_blob(state: &HandlerState, req: BlobRequest) { .list_missing_blobs( repo_id, cursor.as_deref(), - usize::try_from(limit).unwrap_or(usize::MAX), + usize::try_from(limit).unwrap_or(0), ) .map_err(metastore_to_db); let _ = tx.send(result); @@ -1445,6 +3212,1615 @@ fn dispatch_blob(state: &HandlerState, req: BlobRequest) { } } +fn dispatch_delegation(state: &HandlerState, req: DelegationRequest) { + match req { + DelegationRequest::IsDelegatedAccount { did, tx } => { + let result = state + .metastore + .delegation_ops() + .is_delegated_account(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::CreateDelegation { + delegated_did, + controller_did, + granted_scopes, + granted_by, + tx, + } => { + let result = state + .metastore + .delegation_ops() + .create_delegation( + &delegated_did, + &controller_did, + &granted_scopes, + &granted_by, + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::RevokeDelegation { + delegated_did, + controller_did, + revoked_by, + tx, + } => { + let result = state + .metastore + .delegation_ops() + .revoke_delegation(&delegated_did, &controller_did, &revoked_by) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::UpdateDelegationScopes { + delegated_did, + controller_did, + new_scopes, + tx, + } => { + let result = state + .metastore + .delegation_ops() + .update_delegation_scopes(&delegated_did, &controller_did, &new_scopes) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::GetDelegation { + delegated_did, + controller_did, + tx, + } => { + let result = state + .metastore + .delegation_ops() + .get_delegation(&delegated_did, &controller_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::GetDelegationsForAccount { delegated_did, tx } => { + let result = state + .metastore + .delegation_ops() + .get_delegations_for_account(&delegated_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::GetAccountsControlledBy { controller_did, tx } => { + let result = state + .metastore + .delegation_ops() + .get_accounts_controlled_by(&controller_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::CountActiveControllers { delegated_did, tx } => { + let result = state + .metastore + .delegation_ops() + .count_active_controllers(&delegated_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::ControlsAnyAccounts { did, tx } => { + let result = state + .metastore + .delegation_ops() + .controls_any_accounts(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::LogDelegationAction { + delegated_did, + actor_did, + controller_did, + action_type, + action_details, + ip_address, + user_agent, + tx, + } => { + let result = state + .metastore + .delegation_ops() + .log_delegation_action( + &delegated_did, + &actor_did, + controller_did.as_ref(), + action_type, + action_details, + ip_address.as_deref(), + user_agent.as_deref(), + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::GetAuditLogForAccount { + delegated_did, + limit, + offset, + tx, + } => { + let result = state + .metastore + .delegation_ops() + .get_audit_log_for_account(&delegated_did, limit, offset) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + DelegationRequest::CountAuditLogEntries { delegated_did, tx } => { + let result = state + .metastore + .delegation_ops() + .count_audit_log_entries(&delegated_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + } +} + +fn dispatch_sso(state: &HandlerState, req: SsoRequest) { + match req { + SsoRequest::CreateExternalIdentity { + did, + provider, + provider_user_id, + provider_username, + provider_email, + tx, + } => { + let result = state + .metastore + .sso_ops() + .create_external_identity( + &did, + provider, + &provider_user_id, + provider_username.as_deref(), + provider_email.as_deref(), + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::GetExternalIdentityByProvider { + provider, + provider_user_id, + tx, + } => { + let result = state + .metastore + .sso_ops() + .get_external_identity_by_provider(provider, &provider_user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::GetExternalIdentitiesByDid { did, tx } => { + let result = state + .metastore + .sso_ops() + .get_external_identities_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::UpdateExternalIdentityLogin { + id, + provider_username, + provider_email, + tx, + } => { + let result = state + .metastore + .sso_ops() + .update_external_identity_login( + id, + provider_username.as_deref(), + provider_email.as_deref(), + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::DeleteExternalIdentity { id, did, tx } => { + let result = state + .metastore + .sso_ops() + .delete_external_identity(id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::CreateSsoAuthState { + state: sso_state, + request_uri, + provider, + action, + nonce, + code_verifier, + did, + tx, + } => { + let result = state + .metastore + .sso_ops() + .create_sso_auth_state( + &sso_state, + &request_uri, + provider, + action, + nonce.as_deref(), + code_verifier.as_deref(), + did.as_ref(), + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::ConsumeSsoAuthState { + state: sso_state, + tx, + } => { + let result = state + .metastore + .sso_ops() + .consume_sso_auth_state(&sso_state) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::CleanupExpiredSsoAuthStates { tx } => { + let result = state + .metastore + .sso_ops() + .cleanup_expired_sso_auth_states() + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::CreatePendingRegistration { + token, + request_uri, + provider, + provider_user_id, + provider_username, + provider_email, + provider_email_verified, + tx, + } => { + let result = state + .metastore + .sso_ops() + .create_pending_registration( + &token, + &request_uri, + provider, + &provider_user_id, + provider_username.as_deref(), + provider_email.as_deref(), + provider_email_verified, + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::GetPendingRegistration { token, tx } => { + let result = state + .metastore + .sso_ops() + .get_pending_registration(&token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::ConsumePendingRegistration { token, tx } => { + let result = state + .metastore + .sso_ops() + .consume_pending_registration(&token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SsoRequest::CleanupExpiredPendingRegistrations { tx } => { + let result = state + .metastore + .sso_ops() + .cleanup_expired_pending_registrations() + .map_err(metastore_to_db); + let _ = tx.send(result); + } + } +} + +fn dispatch_session(state: &HandlerState, req: SessionRequest) { + match req { + SessionRequest::CreateSession { data, tx } => { + let result = state + .metastore + .session_ops() + .create_session(&data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetSessionByAccessJti { access_jti, tx } => { + let result = state + .metastore + .session_ops() + .get_session_by_access_jti(&access_jti) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetSessionForRefresh { refresh_jti, tx } => { + let result = state + .metastore + .session_ops() + .get_session_for_refresh(&refresh_jti) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::UpdateSessionTokens { + session_id, + new_access_jti, + new_refresh_jti, + new_access_expires_at, + new_refresh_expires_at, + tx, + } => { + let result = state + .metastore + .session_ops() + .update_session_tokens( + session_id, + &new_access_jti, + &new_refresh_jti, + new_access_expires_at, + new_refresh_expires_at, + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::DeleteSessionByAccessJti { access_jti, tx } => { + let result = state + .metastore + .session_ops() + .delete_session_by_access_jti(&access_jti) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::DeleteSessionById { session_id, tx } => { + let result = state + .metastore + .session_ops() + .delete_session_by_id(session_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::DeleteSessionsByDid { did, tx } => { + let result = state + .metastore + .session_ops() + .delete_sessions_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::DeleteSessionsByDidExceptJti { + did, + except_jti, + tx, + } => { + let result = state + .metastore + .session_ops() + .delete_sessions_by_did_except_jti(&did, &except_jti) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::ListSessionsByDid { did, tx } => { + let result = state + .metastore + .session_ops() + .list_sessions_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetSessionAccessJtiById { + session_id, + did, + tx, + } => { + let result = state + .metastore + .session_ops() + .get_session_access_jti_by_id(session_id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::DeleteSessionsByAppPassword { + did, + app_password_name, + tx, + } => { + let result = state + .metastore + .session_ops() + .delete_sessions_by_app_password(&did, &app_password_name) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetSessionJtisByAppPassword { + did, + app_password_name, + tx, + } => { + let result = state + .metastore + .session_ops() + .get_session_jtis_by_app_password(&did, &app_password_name) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::CheckRefreshTokenUsed { refresh_jti, tx } => { + let result = state + .metastore + .session_ops() + .check_refresh_token_used(&refresh_jti) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::MarkRefreshTokenUsed { + refresh_jti, + session_id, + tx, + } => { + let result = state + .metastore + .session_ops() + .mark_refresh_token_used(&refresh_jti, session_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::ListAppPasswords { user_id, tx } => { + let result = state + .metastore + .session_ops() + .list_app_passwords(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetAppPasswordsForLogin { user_id, tx } => { + let result = state + .metastore + .session_ops() + .get_app_passwords_for_login(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetAppPasswordByName { user_id, name, tx } => { + let result = state + .metastore + .session_ops() + .get_app_password_by_name(user_id, &name) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::CreateAppPassword { data, tx } => { + let result = state + .metastore + .session_ops() + .create_app_password(&data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::DeleteAppPassword { user_id, name, tx } => { + let result = state + .metastore + .session_ops() + .delete_app_password(user_id, &name) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::DeleteAppPasswordsByController { + did, + controller_did, + tx, + } => { + let result = state + .metastore + .session_ops() + .delete_app_passwords_by_controller(&did, &controller_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetLastReauthAt { did, tx } => { + let result = state + .metastore + .session_ops() + .get_last_reauth_at(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::UpdateLastReauth { did, tx } => { + let result = state + .metastore + .session_ops() + .update_last_reauth(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetSessionMfaStatus { did, tx } => { + let result = state + .metastore + .session_ops() + .get_session_mfa_status(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::UpdateMfaVerified { did, tx } => { + let result = state + .metastore + .session_ops() + .update_mfa_verified(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::GetAppPasswordHashesByDid { did, tx } => { + let result = state + .metastore + .session_ops() + .get_app_password_hashes_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + SessionRequest::RefreshSessionAtomic { data, tx } => { + let result = state + .metastore + .session_ops() + .refresh_session_atomic(&data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + } +} + +fn dispatch_infra(state: &HandlerState, req: InfraRequest) { + match req { + InfraRequest::EnqueueComms { + user_id, + channel, + comms_type, + recipient, + subject, + body, + metadata, + tx, + } => { + let result = state + .metastore + .infra_ops() + .enqueue_comms( + user_id, + channel, + comms_type, + &recipient, + subject.as_deref(), + &body, + metadata, + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::FetchPendingComms { + now, + batch_size, + tx, + } => { + let result = state + .metastore + .infra_ops() + .fetch_pending_comms(now, batch_size) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::MarkCommsSent { id, tx } => { + let result = state + .metastore + .infra_ops() + .mark_comms_sent(id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::MarkCommsFailed { id, error, tx } => { + let result = state + .metastore + .infra_ops() + .mark_comms_failed(id, &error) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::CreateInviteCode { + code, + use_count, + for_account, + tx, + } => { + let result = state + .metastore + .infra_ops() + .create_invite_code(&code, use_count, for_account.as_ref()) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::CreateInviteCodesBatch { + codes, + use_count, + created_by_user, + for_account, + tx, + } => { + let result = state + .metastore + .infra_ops() + .create_invite_codes_batch(&codes, use_count, created_by_user, for_account.as_ref()) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodeAvailableUses { code, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_code_available_uses(&code) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::ValidateInviteCode { code, tx } => { + let result = state + .metastore + .infra_ops() + .validate_invite_code(&code) + .map(|_| ()); + let _ = tx.send(result); + } + InfraRequest::DecrementInviteCodeUses { code, tx } => { + let validated = ValidatedInviteCode::new_validated(&code); + let result = state + .metastore + .infra_ops() + .decrement_invite_code_uses(&validated) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::RecordInviteCodeUse { + code, + used_by_user, + tx, + } => { + let validated = ValidatedInviteCode::new_validated(&code); + let result = state + .metastore + .infra_ops() + .record_invite_code_use(&validated, used_by_user) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodesForAccount { for_account, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_codes_for_account(&for_account) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodeUses { code, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_code_uses(&code) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DisableInviteCodesByCode { codes, tx } => { + let result = state + .metastore + .infra_ops() + .disable_invite_codes_by_code(&codes) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DisableInviteCodesByAccount { accounts, tx } => { + let result = state + .metastore + .infra_ops() + .disable_invite_codes_by_account(&accounts) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::ListInviteCodes { + cursor, + limit, + sort, + tx, + } => { + let result = state + .metastore + .infra_ops() + .list_invite_codes(cursor.as_deref(), limit, sort) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetUserDidsByIds { user_ids, tx } => { + let result = state + .metastore + .infra_ops() + .get_user_dids_by_ids(&user_ids) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodeUsesBatch { codes, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_code_uses_batch(&codes) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInvitesCreatedByUser { user_id, tx } => { + let result = state + .metastore + .infra_ops() + .get_invites_created_by_user(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodeInfo { code, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_code_info(&code) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodesByUsers { user_ids, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_codes_by_users(&user_ids) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodeUsedByUser { user_id, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_code_used_by_user(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeleteInviteCodeUsesByUser { user_id, tx } => { + let result = state + .metastore + .infra_ops() + .delete_invite_code_uses_by_user(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeleteInviteCodesByUser { user_id, tx } => { + let result = state + .metastore + .infra_ops() + .delete_invite_codes_by_user(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::ReserveSigningKey { + did, + public_key_did_key, + private_key_bytes, + expires_at, + tx, + } => { + let result = state + .metastore + .infra_ops() + .reserve_signing_key( + did.as_ref(), + &public_key_did_key, + &private_key_bytes, + expires_at, + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetReservedSigningKey { + public_key_did_key, + tx, + } => { + let result = state + .metastore + .infra_ops() + .get_reserved_signing_key(&public_key_did_key) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::MarkSigningKeyUsed { key_id, tx } => { + let result = state + .metastore + .infra_ops() + .mark_signing_key_used(key_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::CreateDeletionRequest { + token, + did, + expires_at, + tx, + } => { + let result = state + .metastore + .infra_ops() + .create_deletion_request(&token, &did, expires_at) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetDeletionRequest { token, tx } => { + let result = state + .metastore + .infra_ops() + .get_deletion_request(&token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeleteDeletionRequest { token, tx } => { + let result = state + .metastore + .infra_ops() + .delete_deletion_request(&token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeleteDeletionRequestsByDid { did, tx } => { + let result = state + .metastore + .infra_ops() + .delete_deletion_requests_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::UpsertAccountPreference { + user_id, + name, + value_json, + tx, + } => { + let result = state + .metastore + .infra_ops() + .upsert_account_preference(user_id, &name, value_json) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::InsertAccountPreferenceIfNotExists { + user_id, + name, + value_json, + tx, + } => { + let result = state + .metastore + .infra_ops() + .insert_account_preference_if_not_exists(user_id, &name, value_json) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetServerConfig { key, tx } => { + let result = state + .metastore + .infra_ops() + .get_server_config(&key) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::InsertReport { + id, + reason_type, + reason, + subject_json, + reported_by_did, + created_at, + tx, + } => { + let result = state + .metastore + .infra_ops() + .insert_report( + id, + &reason_type, + reason.as_deref(), + subject_json, + &reported_by_did, + created_at, + ) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeletePlcTokensForUser { user_id, tx } => { + let result = state + .metastore + .infra_ops() + .delete_plc_tokens_for_user(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::InsertPlcToken { + user_id, + token, + expires_at, + tx, + } => { + let result = state + .metastore + .infra_ops() + .insert_plc_token(user_id, &token, expires_at) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetPlcTokenExpiry { user_id, token, tx } => { + let result = state + .metastore + .infra_ops() + .get_plc_token_expiry(user_id, &token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeletePlcToken { user_id, token, tx } => { + let result = state + .metastore + .infra_ops() + .delete_plc_token(user_id, &token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetAccountPreferences { user_id, tx } => { + let result = state + .metastore + .infra_ops() + .get_account_preferences(user_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::ReplaceNamespacePreferences { + user_id, + namespace, + preferences, + tx, + } => { + let result = state + .metastore + .infra_ops() + .replace_namespace_preferences(user_id, &namespace, preferences) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetNotificationHistory { user_id, limit, tx } => { + let result = state + .metastore + .infra_ops() + .get_notification_history(user_id, limit) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetServerConfigs { keys, tx } => { + let key_refs: Vec<&str> = keys.iter().map(String::as_str).collect(); + let result = state + .metastore + .infra_ops() + .get_server_configs(&key_refs) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::UpsertServerConfig { key, value, tx } => { + let result = state + .metastore + .infra_ops() + .upsert_server_config(&key, &value) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeleteServerConfig { key, tx } => { + let result = state + .metastore + .infra_ops() + .delete_server_config(&key) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetBlobStorageKeyByCid { cid, tx } => { + let result = state + .metastore + .infra_ops() + .get_blob_storage_key_by_cid(&cid) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::DeleteBlobByCid { cid, tx } => { + let result = state + .metastore + .infra_ops() + .delete_blob_by_cid(&cid) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetAdminAccountInfoByDid { did, tx } => { + let result = state + .metastore + .infra_ops() + .get_admin_account_info_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetAdminAccountInfosByDids { dids, tx } => { + let result = state + .metastore + .infra_ops() + .get_admin_account_infos_by_dids(&dids) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + InfraRequest::GetInviteCodeUsesByUsers { user_ids, tx } => { + let result = state + .metastore + .infra_ops() + .get_invite_code_uses_by_users(&user_ids) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + } +} + +fn dispatch_oauth(state: &HandlerState, req: OAuthRequest) { + match req { + OAuthRequest::CreateToken { data, tx } => { + let result = state + .metastore + .oauth_ops() + .create_token(&data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetTokenById { token_id, tx } => { + let result = state + .metastore + .oauth_ops() + .get_token_by_id(&token_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetTokenByRefreshToken { refresh_token, tx } => { + let result = state + .metastore + .oauth_ops() + .get_token_by_refresh_token(&refresh_token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetTokenByPreviousRefreshToken { refresh_token, tx } => { + let result = state + .metastore + .oauth_ops() + .get_token_by_previous_refresh_token(&refresh_token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::RotateToken { + old_db_id, + new_refresh_token, + new_expires_at, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .rotate_token(old_db_id, &new_refresh_token, new_expires_at) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CheckRefreshTokenUsed { refresh_token, tx } => { + let result = state + .metastore + .oauth_ops() + .check_refresh_token_used(&refresh_token) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteToken { token_id, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_token(&token_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteTokenFamily { db_id, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_token_family(db_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::ListTokensForUser { did, tx } => { + let result = state + .metastore + .oauth_ops() + .list_tokens_for_user(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CountTokensForUser { did, tx } => { + let result = state + .metastore + .oauth_ops() + .count_tokens_for_user(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteOldestTokensForUser { + did, + keep_count, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .delete_oldest_tokens_for_user(&did, keep_count) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::RevokeTokensForClient { did, client_id, tx } => { + let result = state + .metastore + .oauth_ops() + .revoke_tokens_for_client(&did, &client_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::RevokeTokensForController { + delegated_did, + controller_did, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .revoke_tokens_for_controller(&delegated_did, &controller_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CreateAuthorizationRequest { + request_id, + data, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .create_authorization_request(&request_id, &data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetAuthorizationRequest { request_id, tx } => { + let result = state + .metastore + .oauth_ops() + .get_authorization_request(&request_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::SetAuthorizationDid { + request_id, + did, + device_id, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .set_authorization_did(&request_id, &did, device_id.as_ref()) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::UpdateAuthorizationRequest { + request_id, + did, + device_id, + code, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .update_authorization_request(&request_id, &did, device_id.as_ref(), &code) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::ConsumeAuthorizationRequestByCode { code, tx } => { + let result = state + .metastore + .oauth_ops() + .consume_authorization_request_by_code(&code) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteAuthorizationRequest { request_id, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_authorization_request(&request_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteExpiredAuthorizationRequests { tx } => { + let result = state + .metastore + .oauth_ops() + .delete_expired_authorization_requests() + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::ExtendAuthorizationRequestExpiry { + request_id, + new_expires_at, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .extend_authorization_request_expiry(&request_id, new_expires_at) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::MarkRequestAuthenticated { + request_id, + did, + device_id, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .mark_request_authenticated(&request_id, &did, device_id.as_ref()) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::UpdateRequestScope { + request_id, + scope, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .update_request_scope(&request_id, &scope) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::SetControllerDid { + request_id, + controller_did, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .set_controller_did(&request_id, &controller_did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::SetRequestDid { + request_id, + did, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .set_request_did(&request_id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CreateDevice { + device_id, + data, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .create_device(&device_id, &data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetDevice { device_id, tx } => { + let result = state + .metastore + .oauth_ops() + .get_device(&device_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::UpdateDeviceLastSeen { device_id, tx } => { + let result = state + .metastore + .oauth_ops() + .update_device_last_seen(&device_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteDevice { device_id, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_device(&device_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::UpsertAccountDevice { did, device_id, tx } => { + let result = state + .metastore + .oauth_ops() + .upsert_account_device(&did, &device_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetDeviceAccounts { device_id, tx } => { + let result = state + .metastore + .oauth_ops() + .get_device_accounts(&device_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::VerifyAccountOnDevice { device_id, did, tx } => { + let result = state + .metastore + .oauth_ops() + .verify_account_on_device(&device_id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CheckAndRecordDpopJti { jti, tx } => { + let result = state + .metastore + .oauth_ops() + .check_and_record_dpop_jti(&jti) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CleanupExpiredDpopJtis { max_age_secs, tx } => { + let result = state + .metastore + .oauth_ops() + .cleanup_expired_dpop_jtis(max_age_secs) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::Create2faChallenge { + did, + request_uri, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .create_2fa_challenge(&did, &request_uri) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::Get2faChallenge { request_uri, tx } => { + let result = state + .metastore + .oauth_ops() + .get_2fa_challenge(&request_uri) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::Increment2faAttempts { id, tx } => { + let result = state + .metastore + .oauth_ops() + .increment_2fa_attempts(id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::Delete2faChallenge { id, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_2fa_challenge(id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::Delete2faChallengeByRequestUri { request_uri, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_2fa_challenge_by_request_uri(&request_uri) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CleanupExpired2faChallenges { tx } => { + let result = state + .metastore + .oauth_ops() + .cleanup_expired_2fa_challenges() + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::CheckUser2faEnabled { did, tx } => { + let result = state + .metastore + .oauth_ops() + .check_user_2fa_enabled(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetScopePreferences { did, client_id, tx } => { + let result = state + .metastore + .oauth_ops() + .get_scope_preferences(&did, &client_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::UpsertScopePreferences { + did, + client_id, + prefs, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .upsert_scope_preferences(&did, &client_id, &prefs) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteScopePreferences { did, client_id, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_scope_preferences(&did, &client_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::UpsertAuthorizedClient { + did, + client_id, + data, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .upsert_authorized_client(&did, &client_id, &data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetAuthorizedClient { did, client_id, tx } => { + let result = state + .metastore + .oauth_ops() + .get_authorized_client(&did, &client_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::ListTrustedDevices { did, tx } => { + let result = state + .metastore + .oauth_ops() + .list_trusted_devices(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::GetDeviceTrustInfo { device_id, did, tx } => { + let result = state + .metastore + .oauth_ops() + .get_device_trust_info(&device_id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeviceBelongsToUser { device_id, did, tx } => { + let result = state + .metastore + .oauth_ops() + .device_belongs_to_user(&device_id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::RevokeDeviceTrust { device_id, did, tx } => { + let result = state + .metastore + .oauth_ops() + .revoke_device_trust(&device_id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::UpdateDeviceFriendlyName { + device_id, + did, + friendly_name, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .update_device_friendly_name(&device_id, &did, friendly_name.as_deref()) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::TrustDevice { + device_id, + did, + trusted_at, + trusted_until, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .trust_device(&device_id, &did, trusted_at, trusted_until) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::ExtendDeviceTrust { + device_id, + did, + trusted_until, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .extend_device_trust(&device_id, &did, trusted_until) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::ListSessionsByDid { did, tx } => { + let result = state + .metastore + .oauth_ops() + .list_sessions_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteSessionById { + session_id, + did, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .delete_session_by_id(session_id, &did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteSessionsByDid { did, tx } => { + let result = state + .metastore + .oauth_ops() + .delete_sessions_by_did(&did) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + OAuthRequest::DeleteSessionsByDidExcept { + did, + except_token_id, + tx, + } => { + let result = state + .metastore + .oauth_ops() + .delete_sessions_by_did_except(&did, &except_token_id) + .map_err(metastore_to_db); + let _ = tx.send(result); + } + } +} + fn dispatch(state: &HandlerState, request: MetastoreRequest) { match request { MetastoreRequest::Repo(r) => dispatch_repo(state, r), @@ -1454,6 +4830,811 @@ fn dispatch(state: &HandlerState, request: MetastoreRequest) { MetastoreRequest::Commit(r) => dispatch_commit(state, *r), MetastoreRequest::Backlink(r) => dispatch_backlink(state, r), MetastoreRequest::Blob(r) => dispatch_blob(state, r), + MetastoreRequest::Delegation(r) => dispatch_delegation(state, r), + MetastoreRequest::Sso(r) => dispatch_sso(state, r), + MetastoreRequest::Session(r) => dispatch_session(state, r), + MetastoreRequest::Infra(r) => dispatch_infra(state, r), + MetastoreRequest::OAuth(r) => dispatch_oauth(state, r), + MetastoreRequest::User(r) => dispatch_user(state, r), + } +} + +fn dispatch_user(state: &HandlerState, req: UserRequest) { + let user = state.metastore.user_ops(); + match req { + UserRequest::GetByDid { did, tx } => { + let _ = tx.send(user.get_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetByHandle { handle, tx } => { + let _ = tx.send(user.get_by_handle(&handle).map_err(metastore_to_db)); + } + UserRequest::GetWithKeyByDid { did, tx } => { + let _ = tx.send(user.get_with_key_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetStatusByDid { did, tx } => { + let _ = tx.send(user.get_status_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::CountUsers { tx } => { + let _ = tx.send(user.count_users().map_err(metastore_to_db)); + } + UserRequest::GetSessionAccessExpiry { + did, + access_jti, + tx, + } => { + let _ = tx.send( + user.get_session_access_expiry(&did, &access_jti) + .map_err(metastore_to_db), + ); + } + UserRequest::GetOAuthTokenWithUser { token_id, tx } => { + let _ = tx.send( + user.get_oauth_token_with_user(&token_id) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserInfoByDid { did, tx } => { + let _ = tx.send(user.get_user_info_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetAnyAdminUserId { tx } => { + let _ = tx.send(user.get_any_admin_user_id().map_err(metastore_to_db)); + } + UserRequest::SetInvitesDisabled { did, disabled, tx } => { + let _ = tx.send( + user.set_invites_disabled(&did, disabled) + .map_err(metastore_to_db), + ); + } + UserRequest::SearchAccounts { + cursor_did, + email_filter, + handle_filter, + limit, + tx, + } => { + let _ = tx.send( + user.search_accounts( + cursor_did.as_ref(), + email_filter.as_deref(), + handle_filter.as_deref(), + limit, + ) + .map_err(metastore_to_db), + ); + } + UserRequest::GetAuthInfoByDid { did, tx } => { + let _ = tx.send(user.get_auth_info_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetByEmail { email, tx } => { + let _ = tx.send(user.get_by_email(&email).map_err(metastore_to_db)); + } + UserRequest::GetLoginCheckByHandleOrEmail { identifier, tx } => { + let _ = tx.send( + user.get_login_check_by_handle_or_email(&identifier) + .map_err(metastore_to_db), + ); + } + UserRequest::GetLoginInfoByHandleOrEmail { identifier, tx } => { + let _ = tx.send( + user.get_login_info_by_handle_or_email(&identifier) + .map_err(metastore_to_db), + ); + } + UserRequest::Get2faStatusByDid { did, tx } => { + let _ = tx.send(user.get_2fa_status_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetCommsPrefs { user_id, tx } => { + let _ = tx.send(user.get_comms_prefs(user_id).map_err(metastore_to_db)); + } + UserRequest::GetIdByDid { did, tx } => { + let _ = tx.send(user.get_id_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetUserKeyById { user_id, tx } => { + let _ = tx.send(user.get_user_key_by_id(user_id).map_err(metastore_to_db)); + } + UserRequest::GetIdAndHandleByDid { did, tx } => { + let _ = tx.send(user.get_id_and_handle_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetDidWebInfoByHandle { handle, tx } => { + let _ = tx.send( + user.get_did_web_info_by_handle(&handle) + .map_err(metastore_to_db), + ); + } + UserRequest::GetDidWebOverrides { user_id, tx } => { + let _ = tx.send(user.get_did_web_overrides(user_id).map_err(metastore_to_db)); + } + UserRequest::GetHandleByDid { did, tx } => { + let _ = tx.send(user.get_handle_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::IsAccountActiveByDid { did, tx } => { + let _ = tx.send(user.is_account_active_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetUserForDeletion { did, tx } => { + let _ = tx.send(user.get_user_for_deletion(&did).map_err(metastore_to_db)); + } + UserRequest::CheckHandleExists { + handle, + exclude_user_id, + tx, + } => { + let _ = tx.send( + user.check_handle_exists(&handle, exclude_user_id) + .map_err(metastore_to_db), + ); + } + UserRequest::UpdateHandle { + user_id, + handle, + tx, + } => { + let _ = tx.send( + user.update_handle(user_id, &handle) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserWithKeyByDid { did, tx } => { + let _ = tx.send(user.get_user_with_key_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::IsAccountMigrated { did, tx } => { + let _ = tx.send(user.is_account_migrated(&did).map_err(metastore_to_db)); + } + UserRequest::HasVerifiedCommsChannel { did, tx } => { + let _ = tx.send( + user.has_verified_comms_channel(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::GetIdByHandle { handle, tx } => { + let _ = tx.send(user.get_id_by_handle(&handle).map_err(metastore_to_db)); + } + UserRequest::GetEmailInfoByDid { did, tx } => { + let _ = tx.send(user.get_email_info_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::CheckEmailExists { + email, + exclude_user_id, + tx, + } => { + let _ = tx.send( + user.check_email_exists(&email, exclude_user_id) + .map_err(metastore_to_db), + ); + } + UserRequest::UpdateEmail { user_id, email, tx } => { + let _ = tx.send(user.update_email(user_id, &email).map_err(metastore_to_db)); + } + UserRequest::SetEmailVerified { + user_id, + verified, + tx, + } => { + let _ = tx.send( + user.set_email_verified(user_id, verified) + .map_err(metastore_to_db), + ); + } + UserRequest::CheckEmailVerifiedByIdentifier { identifier, tx } => { + let _ = tx.send( + user.check_email_verified_by_identifier(&identifier) + .map_err(metastore_to_db), + ); + } + UserRequest::CheckChannelVerifiedByDid { did, channel, tx } => { + let _ = tx.send( + user.check_channel_verified_by_did(&did, channel) + .map_err(metastore_to_db), + ); + } + UserRequest::AdminUpdateEmail { did, email, tx } => { + let _ = tx.send( + user.admin_update_email(&did, &email) + .map_err(metastore_to_db), + ); + } + UserRequest::AdminUpdateHandle { did, handle, tx } => { + let _ = tx.send( + user.admin_update_handle(&did, &handle) + .map_err(metastore_to_db), + ); + } + UserRequest::AdminUpdatePassword { + did, + password_hash, + tx, + } => { + let _ = tx.send( + user.admin_update_password(&did, &password_hash) + .map_err(metastore_to_db), + ); + } + UserRequest::GetNotificationPrefs { did, tx } => { + let _ = tx.send(user.get_notification_prefs(&did).map_err(metastore_to_db)); + } + UserRequest::GetIdHandleEmailByDid { did, tx } => { + let _ = tx.send( + user.get_id_handle_email_by_did(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::UpdatePreferredCommsChannel { did, channel, tx } => { + let _ = tx.send( + user.update_preferred_comms_channel(&did, channel) + .map_err(metastore_to_db), + ); + } + UserRequest::ClearDiscord { user_id, tx } => { + let _ = tx.send(user.clear_discord(user_id).map_err(metastore_to_db)); + } + UserRequest::ClearTelegram { user_id, tx } => { + let _ = tx.send(user.clear_telegram(user_id).map_err(metastore_to_db)); + } + UserRequest::ClearSignal { user_id, tx } => { + let _ = tx.send(user.clear_signal(user_id).map_err(metastore_to_db)); + } + UserRequest::SetUnverifiedSignal { + user_id, + signal_username, + tx, + } => { + let _ = tx.send( + user.set_unverified_signal(user_id, &signal_username) + .map_err(metastore_to_db), + ); + } + UserRequest::SetUnverifiedTelegram { + user_id, + telegram_username, + tx, + } => { + let _ = tx.send( + user.set_unverified_telegram(user_id, &telegram_username) + .map_err(metastore_to_db), + ); + } + UserRequest::StoreTelegramChatId { + telegram_username, + chat_id, + handle, + tx, + } => { + let _ = tx.send( + user.store_telegram_chat_id(&telegram_username, chat_id, handle.as_deref()) + .map_err(metastore_to_db), + ); + } + UserRequest::GetTelegramChatId { user_id, tx } => { + let _ = tx.send(user.get_telegram_chat_id(user_id).map_err(metastore_to_db)); + } + UserRequest::SetUnverifiedDiscord { + user_id, + discord_username, + tx, + } => { + let _ = tx.send( + user.set_unverified_discord(user_id, &discord_username) + .map_err(metastore_to_db), + ); + } + UserRequest::StoreDiscordUserId { + discord_username, + discord_id, + handle, + tx, + } => { + let _ = tx.send( + user.store_discord_user_id(&discord_username, &discord_id, handle.as_deref()) + .map_err(metastore_to_db), + ); + } + UserRequest::GetVerificationInfo { did, tx } => { + let _ = tx.send(user.get_verification_info(&did).map_err(metastore_to_db)); + } + UserRequest::VerifyEmailChannel { user_id, email, tx } => { + let _ = tx.send( + user.verify_email_channel(user_id, &email) + .map_err(metastore_to_db), + ); + } + UserRequest::VerifyDiscordChannel { + user_id, + discord_id, + tx, + } => { + let _ = tx.send( + user.verify_discord_channel(user_id, &discord_id) + .map_err(metastore_to_db), + ); + } + UserRequest::VerifyTelegramChannel { + user_id, + telegram_username, + tx, + } => { + let _ = tx.send( + user.verify_telegram_channel(user_id, &telegram_username) + .map_err(metastore_to_db), + ); + } + UserRequest::VerifySignalChannel { + user_id, + signal_username, + tx, + } => { + let _ = tx.send( + user.verify_signal_channel(user_id, &signal_username) + .map_err(metastore_to_db), + ); + } + UserRequest::SetEmailVerifiedFlag { user_id, tx } => { + let _ = tx.send( + user.set_email_verified_flag(user_id) + .map_err(metastore_to_db), + ); + } + UserRequest::SetDiscordVerifiedFlag { user_id, tx } => { + let _ = tx.send( + user.set_discord_verified_flag(user_id) + .map_err(metastore_to_db), + ); + } + UserRequest::SetTelegramVerifiedFlag { user_id, tx } => { + let _ = tx.send( + user.set_telegram_verified_flag(user_id) + .map_err(metastore_to_db), + ); + } + UserRequest::SetSignalVerifiedFlag { user_id, tx } => { + let _ = tx.send( + user.set_signal_verified_flag(user_id) + .map_err(metastore_to_db), + ); + } + UserRequest::HasTotpEnabled { did, tx } => { + let _ = tx.send(user.has_totp_enabled(&did).map_err(metastore_to_db)); + } + UserRequest::HasPasskeys { did, tx } => { + let _ = tx.send(user.has_passkeys(&did).map_err(metastore_to_db)); + } + UserRequest::GetPasswordHashByDid { did, tx } => { + let _ = tx.send(user.get_password_hash_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetPasskeysForUser { did, tx } => { + let _ = tx.send(user.get_passkeys_for_user(&did).map_err(metastore_to_db)); + } + UserRequest::GetPasskeyByCredentialId { credential_id, tx } => { + let _ = tx.send( + user.get_passkey_by_credential_id(&credential_id) + .map_err(metastore_to_db), + ); + } + UserRequest::SavePasskey { + did, + credential_id, + public_key, + friendly_name, + tx, + } => { + let _ = tx.send( + user.save_passkey(&did, &credential_id, &public_key, friendly_name.as_deref()) + .map_err(metastore_to_db), + ); + } + UserRequest::UpdatePasskeyCounter { + credential_id, + new_counter, + tx, + } => { + let _ = tx.send( + user.update_passkey_counter(&credential_id, new_counter) + .map_err(metastore_to_db), + ); + } + UserRequest::DeletePasskey { id, did, tx } => { + let _ = tx.send(user.delete_passkey(id, &did).map_err(metastore_to_db)); + } + UserRequest::UpdatePasskeyName { id, did, name, tx } => { + let _ = tx.send( + user.update_passkey_name(id, &did, &name) + .map_err(metastore_to_db), + ); + } + UserRequest::SaveWebauthnChallenge { + did, + challenge_type, + state_json, + tx, + } => { + let _ = tx.send( + user.save_webauthn_challenge(&did, challenge_type, &state_json) + .map_err(metastore_to_db), + ); + } + UserRequest::LoadWebauthnChallenge { + did, + challenge_type, + tx, + } => { + let _ = tx.send( + user.load_webauthn_challenge(&did, challenge_type) + .map_err(metastore_to_db), + ); + } + UserRequest::DeleteWebauthnChallenge { + did, + challenge_type, + tx, + } => { + let _ = tx.send( + user.delete_webauthn_challenge(&did, challenge_type) + .map_err(metastore_to_db), + ); + } + UserRequest::GetTotpRecord { did, tx } => { + let _ = tx.send(user.get_totp_record(&did).map_err(metastore_to_db)); + } + UserRequest::GetTotpRecordState { did, tx } => { + let _ = tx.send(user.get_totp_record_state(&did).map_err(metastore_to_db)); + } + UserRequest::UpsertTotpSecret { + did, + secret_encrypted, + encryption_version, + tx, + } => { + let _ = tx.send( + user.upsert_totp_secret(&did, &secret_encrypted, encryption_version) + .map_err(metastore_to_db), + ); + } + UserRequest::SetTotpVerified { did, tx } => { + let _ = tx.send(user.set_totp_verified(&did).map_err(metastore_to_db)); + } + UserRequest::UpdateTotpLastUsed { did, tx } => { + let _ = tx.send(user.update_totp_last_used(&did).map_err(metastore_to_db)); + } + UserRequest::DeleteTotp { did, tx } => { + let _ = tx.send(user.delete_totp(&did).map_err(metastore_to_db)); + } + UserRequest::GetUnusedBackupCodes { did, tx } => { + let _ = tx.send(user.get_unused_backup_codes(&did).map_err(metastore_to_db)); + } + UserRequest::MarkBackupCodeUsed { code_id, tx } => { + let _ = tx.send(user.mark_backup_code_used(code_id).map_err(metastore_to_db)); + } + UserRequest::CountUnusedBackupCodes { did, tx } => { + let _ = tx.send( + user.count_unused_backup_codes(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::DeleteBackupCodes { did, tx } => { + let _ = tx.send(user.delete_backup_codes(&did).map_err(metastore_to_db)); + } + UserRequest::InsertBackupCodes { + did, + code_hashes, + tx, + } => { + let _ = tx.send( + user.insert_backup_codes(&did, &code_hashes) + .map_err(metastore_to_db), + ); + } + UserRequest::EnableTotpWithBackupCodes { + did, + code_hashes, + tx, + } => { + let _ = tx.send( + user.enable_totp_with_backup_codes(&did, &code_hashes) + .map_err(metastore_to_db), + ); + } + UserRequest::DeleteTotpAndBackupCodes { did, tx } => { + let _ = tx.send( + user.delete_totp_and_backup_codes(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::ReplaceBackupCodes { + did, + code_hashes, + tx, + } => { + let _ = tx.send( + user.replace_backup_codes(&did, &code_hashes) + .map_err(metastore_to_db), + ); + } + UserRequest::GetSessionInfoByDid { did, tx } => { + let _ = tx.send(user.get_session_info_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetLegacyLoginPref { did, tx } => { + let _ = tx.send(user.get_legacy_login_pref(&did).map_err(metastore_to_db)); + } + UserRequest::UpdateLegacyLogin { did, allow, tx } => { + let _ = tx.send( + user.update_legacy_login(&did, allow) + .map_err(metastore_to_db), + ); + } + UserRequest::UpdateLocale { did, locale, tx } => { + let _ = tx.send(user.update_locale(&did, &locale).map_err(metastore_to_db)); + } + UserRequest::GetLoginFullByIdentifier { identifier, tx } => { + let _ = tx.send( + user.get_login_full_by_identifier(&identifier) + .map_err(metastore_to_db), + ); + } + UserRequest::GetConfirmSignupByDid { did, tx } => { + let _ = tx.send( + user.get_confirm_signup_by_did(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::GetResendVerificationByDid { did, tx } => { + let _ = tx.send( + user.get_resend_verification_by_did(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::SetChannelVerified { did, channel, tx } => { + let _ = tx.send( + user.set_channel_verified(&did, channel) + .map_err(metastore_to_db), + ); + } + UserRequest::GetIdByEmailOrHandle { email, handle, tx } => { + let _ = tx.send( + user.get_id_by_email_or_handle(&email, &handle) + .map_err(metastore_to_db), + ); + } + UserRequest::CountAccountsByEmail { email, tx } => { + let _ = tx.send( + user.count_accounts_by_email(&email) + .map_err(metastore_to_db), + ); + } + UserRequest::GetHandlesByEmail { email, tx } => { + let _ = tx.send(user.get_handles_by_email(&email).map_err(metastore_to_db)); + } + UserRequest::SetPasswordResetCode { + user_id, + code, + expires_at, + tx, + } => { + let _ = tx.send( + user.set_password_reset_code(user_id, &code, expires_at) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserByResetCode { code, tx } => { + let _ = tx.send(user.get_user_by_reset_code(&code).map_err(metastore_to_db)); + } + UserRequest::ClearPasswordResetCode { user_id, tx } => { + let _ = tx.send( + user.clear_password_reset_code(user_id) + .map_err(metastore_to_db), + ); + } + UserRequest::GetIdAndPasswordHashByDid { did, tx } => { + let _ = tx.send( + user.get_id_and_password_hash_by_did(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::UpdatePasswordHash { + user_id, + password_hash, + tx, + } => { + let _ = tx.send( + user.update_password_hash(user_id, &password_hash) + .map_err(metastore_to_db), + ); + } + UserRequest::ResetPasswordWithSessions { + user_id, + password_hash, + tx, + } => { + let _ = tx.send( + user.reset_password_with_sessions(user_id, &password_hash) + .map_err(metastore_to_db), + ); + } + UserRequest::ActivateAccount { did, tx } => { + let _ = tx.send(user.activate_account(&did).map_err(metastore_to_db)); + } + UserRequest::DeactivateAccount { + did, + delete_after, + tx, + } => { + let _ = tx.send( + user.deactivate_account(&did, delete_after) + .map_err(metastore_to_db), + ); + } + UserRequest::HasPasswordByDid { did, tx } => { + let _ = tx.send(user.has_password_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::GetPasswordInfoByDid { did, tx } => { + let _ = tx.send(user.get_password_info_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::RemoveUserPassword { user_id, tx } => { + let _ = tx.send(user.remove_user_password(user_id).map_err(metastore_to_db)); + } + UserRequest::SetNewUserPassword { + user_id, + password_hash, + tx, + } => { + let _ = tx.send( + user.set_new_user_password(user_id, &password_hash) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserKeyByDid { did, tx } => { + let _ = tx.send(user.get_user_key_by_did(&did).map_err(metastore_to_db)); + } + UserRequest::DeleteAccountComplete { user_id, did, tx } => { + let _ = tx.send( + user.delete_account_complete(user_id, &did) + .map_err(metastore_to_db), + ); + } + UserRequest::SetUserTakedown { + did, + takedown_ref, + tx, + } => { + let _ = tx.send( + user.set_user_takedown(&did, takedown_ref.as_deref()) + .map_err(metastore_to_db), + ); + } + UserRequest::AdminDeleteAccountComplete { user_id, did, tx } => { + let _ = tx.send( + user.admin_delete_account_complete(user_id, &did) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserForDidDoc { did, tx } => { + let _ = tx.send(user.get_user_for_did_doc(&did).map_err(metastore_to_db)); + } + UserRequest::GetUserForDidDocBuild { did, tx } => { + let _ = tx.send( + user.get_user_for_did_doc_build(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::UpsertDidWebOverrides { + user_id, + verification_methods, + also_known_as, + tx, + } => { + let _ = tx.send( + user.upsert_did_web_overrides(user_id, verification_methods, also_known_as) + .map_err(metastore_to_db), + ); + } + UserRequest::UpdateMigratedToPds { did, endpoint, tx } => { + let _ = tx.send( + user.update_migrated_to_pds(&did, &endpoint) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserForPasskeySetup { did, tx } => { + let _ = tx.send( + user.get_user_for_passkey_setup(&did) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserForPasskeyRecovery { + identifier, + normalized_handle, + tx, + } => { + let _ = tx.send( + user.get_user_for_passkey_recovery(&identifier, &normalized_handle) + .map_err(metastore_to_db), + ); + } + UserRequest::SetRecoveryToken { + did, + token_hash, + expires_at, + tx, + } => { + let _ = tx.send( + user.set_recovery_token(&did, &token_hash, expires_at) + .map_err(metastore_to_db), + ); + } + UserRequest::GetUserForRecovery { did, tx } => { + let _ = tx.send(user.get_user_for_recovery(&did).map_err(metastore_to_db)); + } + UserRequest::GetAccountsScheduledForDeletion { limit, tx } => { + let _ = tx.send( + user.get_accounts_scheduled_for_deletion(limit) + .map_err(metastore_to_db), + ); + } + UserRequest::DeleteAccountWithFirehose { user_id, did, tx } => { + let _ = tx.send( + user.delete_account_with_firehose(user_id, &did) + .map_err(metastore_to_db), + ); + } + UserRequest::CreatePasswordAccount { input, tx } => { + let _ = tx.send(user.create_password_account(&input)); + } + UserRequest::CreateDelegatedAccount { input, tx } => { + let _ = tx.send(user.create_delegated_account(&input)); + } + UserRequest::CreatePasskeyAccount { input, tx } => { + let _ = tx.send(user.create_passkey_account(&input)); + } + UserRequest::CreateSsoAccount { input, tx } => { + let _ = tx.send(user.create_sso_account(&input)); + } + UserRequest::ReactivateMigrationAccount { input, tx } => { + let _ = tx.send(user.reactivate_migration_account(&input)); + } + UserRequest::CheckHandleAvailableForNewAccount { handle, tx } => { + let _ = tx.send( + user.check_handle_available_for_new_account(&handle) + .map_err(metastore_to_db), + ); + } + UserRequest::ReserveHandle { + handle, + reserved_by, + tx, + } => { + let _ = tx.send( + user.reserve_handle(&handle, &reserved_by) + .map_err(metastore_to_db), + ); + } + UserRequest::ReleaseHandleReservation { handle, tx } => { + let _ = tx.send( + user.release_handle_reservation(&handle) + .map_err(metastore_to_db), + ); + } + UserRequest::CleanupExpiredHandleReservations { tx } => { + let _ = tx.send( + user.cleanup_expired_handle_reservations() + .map_err(metastore_to_db), + ); + } + UserRequest::CheckAndConsumeInviteCode { code, tx } => { + let infra = state.metastore.infra_ops(); + let result = match infra.validate_invite_code(&code) { + Ok(validated) => infra + .decrement_invite_code_uses(&validated) + .map(|()| true) + .map_err(metastore_to_db), + Err(_) => Ok(false), + }; + let _ = tx.send(result); + } + UserRequest::CompletePasskeySetup { input, tx } => { + let _ = tx.send(user.complete_passkey_setup(&input).map_err(metastore_to_db)); + } + UserRequest::RecoverPasskeyAccount { input, tx } => { + let _ = tx.send( + user.recover_passkey_account(&input) + .map_err(metastore_to_db), + ); + } } } @@ -1497,8 +5678,9 @@ const DEFAULT_CHANNEL_BOUND: usize = 256; const MAX_REPOS_WITHOUT_REV: usize = 10_000; pub struct HandlerPool { - senders: Vec>, - handles: Option>>, + senders: parking_lot::Mutex>>, + handles: parking_lot::Mutex>>>, + sender_count: usize, user_hashes: Arc, round_robin: AtomicUsize, } @@ -1535,21 +5717,26 @@ impl HandlerPool { .unzip(); Self { - senders, - handles: Some(handles), + sender_count: senders.len(), + senders: parking_lot::Mutex::new(senders), + handles: parking_lot::Mutex::new(Some(handles)), user_hashes, round_robin: AtomicUsize::new(0), } } pub fn send(&self, request: MetastoreRequest) -> Result<(), DbError> { + let senders = self.senders.lock(); + if senders.is_empty() { + return Err(DbError::Connection( + "metastore handler pool shut down".to_string(), + )); + } let index = match request.routing(&self.user_hashes) { - Routing::Sharded(bits) => (bits as usize) % self.senders.len(), - Routing::Global => { - self.round_robin.fetch_add(1, Ordering::Relaxed) % self.senders.len() - } + Routing::Sharded(bits) => (bits as usize) % senders.len(), + Routing::Global => self.round_robin.fetch_add(1, Ordering::Relaxed) % senders.len(), }; - self.senders[index].try_send(request).map_err(|e| match e { + senders[index].try_send(request).map_err(|e| match e { flume::TrySendError::Full(_) => { DbError::Query("metastore handler backpressure".to_string()) } @@ -1560,12 +5747,15 @@ impl HandlerPool { } pub fn thread_count(&self) -> usize { - self.senders.len() + self.sender_count } - pub async fn shutdown(&mut self) { - self.senders.clear(); - if let Some(handles) = self.handles.take() { + pub async fn close(&self) { + { + self.senders.lock().clear(); + } + let handles = { self.handles.lock().take() }; + if let Some(handles) = handles { let join_fut = tokio::task::spawn_blocking(move || { handles.into_iter().for_each(|h| { if let Err(e) = h.join() { @@ -1583,8 +5773,8 @@ impl HandlerPool { impl Drop for HandlerPool { fn drop(&mut self) { - self.senders.clear(); - if let Some(handles) = self.handles.take() { + self.senders.get_mut().clear(); + if let Some(handles) = self.handles.get_mut().take() { tracing::warn!( "HandlerPool dropped without calling shutdown(); blocking on thread join" ); @@ -1705,7 +5895,7 @@ mod tests { #[tokio::test] async fn shutdown_completes_inflight() { - let mut h = setup(); + let h = setup(); let user_id = Uuid::new_v4(); let did = Did::from("did:plc:shutdown_test".to_string()); let handle = Handle::from("shutdown.test.invalid".to_string()); @@ -1724,6 +5914,6 @@ mod tests { .unwrap(); rx.await.unwrap().unwrap(); - h.pool.shutdown().await; + h.pool.close().await; } } diff --git a/crates/tranquil-store/src/metastore/infra_ops.rs b/crates/tranquil-store/src/metastore/infra_ops.rs new file mode 100644 index 0000000..e9040cb --- /dev/null +++ b/crates/tranquil-store/src/metastore/infra_ops.rs @@ -0,0 +1,1209 @@ +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use fjall::{Database, Keyspace}; +use uuid::Uuid; + +use super::MetastoreError; +use super::blobs::{BlobMetaValue, blob_by_cid_key, blob_meta_key}; +use super::infra_schema::{ + DeletionRequestValue, InviteCodeUseValue, InviteCodeValue, NotificationHistoryValue, + QueuedCommsValue, ReportValue, SigningKeyValue, account_pref_key, account_pref_prefix, + channel_to_u8, comms_history_key, comms_history_prefix, comms_queue_key, comms_queue_prefix, + comms_type_to_u8, deletion_by_did_key, deletion_request_key, invite_by_user_key, + invite_code_key, invite_code_prefix, invite_code_used_by_key, invite_use_key, + invite_use_prefix, plc_token_key, plc_token_prefix, report_key, server_config_key, + signing_key_by_id_key, signing_key_key, status_to_u8, u8_to_channel, u8_to_comms_type, + u8_to_status, +}; +use super::keys::UserHash; +use super::scan::{delete_all_by_prefix, point_lookup}; +use super::user_hash::UserHashMap; +use super::users::UserValue; + +use tranquil_db_traits::{ + AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest, InviteCodeError, + InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, InviteCodeState, InviteCodeUse, + NotificationHistoryRow, QueuedComms, ReservedSigningKey, ValidatedInviteCode, +}; +use tranquil_types::{CidLink, Did, Handle}; + +pub struct InfraOps { + db: Database, + infra: Keyspace, + repo_data: Keyspace, + users: Keyspace, + user_hashes: Arc, +} + +impl InfraOps { + pub fn new( + db: Database, + infra: Keyspace, + repo_data: Keyspace, + users: Keyspace, + user_hashes: Arc, + ) -> Self { + Self { + db, + infra, + repo_data, + users, + user_hashes, + } + } + + fn resolve_user_value(&self, user_hash: UserHash) -> Option { + let key = super::encoding::KeyBuilder::new() + .tag(super::keys::KeyTag::USER_PRIMARY) + .u64(user_hash.raw()) + .build(); + self.users + .get(key.as_slice()) + .ok() + .flatten() + .and_then(|raw| UserValue::deserialize(&raw)) + } + + fn resolve_did_for_uuid(&self, user_id: Uuid) -> Option { + let user_hash = self.user_hashes.get(&user_id)?; + self.resolve_user_value(user_hash) + .and_then(|u| Did::new(u.did).ok()) + } + + fn resolve_handle_for_uuid(&self, user_id: Uuid) -> Option { + let user_hash = self.user_hashes.get(&user_id)?; + self.resolve_user_value(user_hash) + .and_then(|u| Handle::new(u.handle).ok()) + } + + fn value_to_queued_comms(&self, v: &QueuedCommsValue) -> Result { + let channel = + u8_to_channel(v.channel).ok_or(MetastoreError::CorruptData("invalid comms channel"))?; + let comms_type = u8_to_comms_type(v.comms_type) + .ok_or(MetastoreError::CorruptData("invalid comms type"))?; + let status = + u8_to_status(v.status).ok_or(MetastoreError::CorruptData("invalid comms status"))?; + Ok(QueuedComms { + id: v.id, + user_id: v.user_id, + channel, + comms_type, + status, + recipient: v.recipient.clone(), + subject: v.subject.clone(), + body: v.body.clone(), + metadata: v + .metadata + .as_ref() + .and_then(|b| serde_json::from_slice(b).ok()), + attempts: v.attempts, + max_attempts: v.max_attempts, + last_error: v.error_message.clone(), + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + updated_at: DateTime::from_timestamp_millis(v.sent_at_ms.unwrap_or(v.created_at_ms)) + .unwrap_or_default(), + scheduled_for: DateTime::from_timestamp_millis(v.scheduled_for_ms).unwrap_or_default(), + processed_at: v.sent_at_ms.and_then(DateTime::from_timestamp_millis), + }) + } + + fn value_to_invite_info(&self, v: &InviteCodeValue) -> Result { + Ok(InviteCodeInfo { + code: v.code.clone(), + available_uses: v.available_uses, + state: InviteCodeState::from_disabled_flag(v.disabled), + for_account: v + .for_account + .as_ref() + .and_then(|d| Did::new(d.clone()).ok()), + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + created_by: v.created_by.and_then(|uid| self.resolve_did_for_uuid(uid)), + }) + } + + fn user_to_admin_info(&self, u: &UserValue) -> Result { + let did = Did::new(u.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid did in user record"))?; + let handle = Handle::new(u.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid handle in user record"))?; + Ok(AdminAccountInfo { + id: u.id, + did, + handle, + email: u.email.clone(), + created_at: DateTime::from_timestamp_millis(u.created_at_ms).unwrap_or_default(), + invites_disabled: u.invites_disabled, + email_verified: u.email_verified, + deactivated_at: u + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + }) + } + + #[allow(clippy::too_many_arguments)] + pub fn enqueue_comms( + &self, + user_id: Option, + channel: CommsChannel, + comms_type: CommsType, + recipient: &str, + subject: Option<&str>, + body: &str, + metadata: Option, + ) -> Result { + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = QueuedCommsValue { + id, + user_id, + channel: channel_to_u8(channel), + comms_type: comms_type_to_u8(comms_type), + recipient: recipient.to_owned(), + subject: subject.map(str::to_owned), + body: body.to_owned(), + metadata: metadata.map(|v| serde_json::to_vec(&v).unwrap_or_default()), + status: status_to_u8(CommsStatus::Pending), + error_message: None, + attempts: 0, + max_attempts: 3, + created_at_ms: now_ms, + scheduled_for_ms: now_ms, + sent_at_ms: None, + }; + + let queue_key = comms_queue_key(id); + let mut batch = self.db.batch(); + batch.insert(&self.infra, queue_key.as_slice(), value.serialize()); + + if let Some(uid) = user_id { + let history_value = NotificationHistoryValue { + id, + channel: channel_to_u8(channel), + comms_type: comms_type_to_u8(comms_type), + recipient: recipient.to_owned(), + subject: subject.map(str::to_owned), + body: body.to_owned(), + status: status_to_u8(CommsStatus::Pending), + created_at_ms: now_ms, + }; + let history_key = comms_history_key(uid, now_ms, id); + batch.insert( + &self.infra, + history_key.as_slice(), + history_value.serialize(), + ); + } + + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(id) + } + + pub fn fetch_pending_comms( + &self, + now: DateTime, + batch_size: i64, + ) -> Result, MetastoreError> { + let now_ms = now.timestamp_millis(); + let limit = usize::try_from(batch_size).unwrap_or(0); + let prefix = comms_queue_prefix(); + + self.infra + .prefix(prefix.as_slice()) + .map(|guard| -> Result, MetastoreError> { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = QueuedCommsValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt comms queue entry"))?; + let is_pending = val.status == status_to_u8(CommsStatus::Pending); + let is_scheduled = val.scheduled_for_ms <= now_ms; + match is_pending && is_scheduled { + true => Ok(Some(self.value_to_queued_comms(&val)?)), + false => Ok(None), + } + }) + .filter_map(Result::transpose) + .take(limit) + .collect() + } + + pub fn mark_comms_sent(&self, id: Uuid) -> Result<(), MetastoreError> { + let key = comms_queue_key(id); + let mut val: QueuedCommsValue = point_lookup( + &self.infra, + key.as_slice(), + QueuedCommsValue::deserialize, + "corrupt comms queue entry", + )? + .ok_or(MetastoreError::InvalidInput("comms entry not found"))?; + + val.status = status_to_u8(CommsStatus::Sent); + val.sent_at_ms = Some(Utc::now().timestamp_millis()); + val.attempts = val.attempts.saturating_add(1); + + let mut batch = self.db.batch(); + batch.insert(&self.infra, key.as_slice(), val.serialize()); + + let history_key = comms_history_key( + val.user_id.unwrap_or(Uuid::nil()), + val.created_at_ms, + val.id, + ); + if let Some(mut history_val) = point_lookup( + &self.infra, + history_key.as_slice(), + NotificationHistoryValue::deserialize, + "corrupt notification history", + )? { + history_val.status = status_to_u8(CommsStatus::Sent); + batch.insert(&self.infra, history_key.as_slice(), history_val.serialize()); + } + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn mark_comms_failed(&self, id: Uuid, error: &str) -> Result<(), MetastoreError> { + let key = comms_queue_key(id); + let mut val: QueuedCommsValue = point_lookup( + &self.infra, + key.as_slice(), + QueuedCommsValue::deserialize, + "corrupt comms queue entry", + )? + .ok_or(MetastoreError::InvalidInput("comms entry not found"))?; + + val.status = status_to_u8(CommsStatus::Failed); + val.error_message = Some(error.to_owned()); + val.attempts = val.attempts.saturating_add(1); + + let mut batch = self.db.batch(); + batch.insert(&self.infra, key.as_slice(), val.serialize()); + + let history_key = comms_history_key( + val.user_id.unwrap_or(Uuid::nil()), + val.created_at_ms, + val.id, + ); + if let Some(mut history_val) = point_lookup( + &self.infra, + history_key.as_slice(), + NotificationHistoryValue::deserialize, + "corrupt notification history", + )? { + history_val.status = status_to_u8(CommsStatus::Failed); + batch.insert(&self.infra, history_key.as_slice(), history_val.serialize()); + } + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn create_invite_code( + &self, + code: &str, + use_count: i32, + for_account: Option<&Did>, + ) -> Result { + let key = invite_code_key(code); + let existing = self + .infra + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)?; + if existing.is_some() { + return Ok(false); + } + + let value = InviteCodeValue { + code: code.to_owned(), + available_uses: use_count, + disabled: false, + for_account: for_account.map(|d| d.to_string()), + created_by: None, + created_at_ms: Utc::now().timestamp_millis(), + }; + + self.infra + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + + pub fn create_invite_codes_batch( + &self, + codes: &[String], + use_count: i32, + created_by_user: Uuid, + for_account: Option<&Did>, + ) -> Result<(), MetastoreError> { + let now_ms = Utc::now().timestamp_millis(); + let mut batch = self.db.batch(); + + codes.iter().try_for_each(|code| { + let value = InviteCodeValue { + code: code.clone(), + available_uses: use_count, + disabled: false, + for_account: for_account.map(|d| d.to_string()), + created_by: Some(created_by_user), + created_at_ms: now_ms, + }; + let key = invite_code_key(code); + batch.insert(&self.infra, key.as_slice(), value.serialize()); + + Ok::<(), MetastoreError>(()) + })?; + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_invite_code_available_uses( + &self, + code: &str, + ) -> Result, MetastoreError> { + let key = invite_code_key(code); + let val: Option = point_lookup( + &self.infra, + key.as_slice(), + InviteCodeValue::deserialize, + "corrupt invite code", + )?; + Ok(val.map(|v| v.available_uses)) + } + + pub fn validate_invite_code<'a>( + &self, + code: &'a str, + ) -> Result, InviteCodeError> { + let key = invite_code_key(code); + let val: Option = point_lookup( + &self.infra, + key.as_slice(), + InviteCodeValue::deserialize, + "corrupt invite code", + ) + .map_err(|e| { + InviteCodeError::DatabaseError(tranquil_db_traits::DbError::Query(e.to_string())) + })?; + + match val { + None => Err(InviteCodeError::NotFound), + Some(v) if v.disabled => Err(InviteCodeError::Disabled), + Some(v) if v.available_uses <= 0 => Err(InviteCodeError::ExhaustedUses), + Some(_) => Ok(ValidatedInviteCode::new_validated(code)), + } + } + + pub fn decrement_invite_code_uses( + &self, + code: &ValidatedInviteCode<'_>, + ) -> Result<(), MetastoreError> { + let key = invite_code_key(code.code()); + let mut val: InviteCodeValue = point_lookup( + &self.infra, + key.as_slice(), + InviteCodeValue::deserialize, + "corrupt invite code", + )? + .ok_or(MetastoreError::InvalidInput("invite code not found"))?; + + val.available_uses = val.available_uses.saturating_sub(1); + self.infra + .insert(key.as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn record_invite_code_use( + &self, + code: &ValidatedInviteCode<'_>, + used_by_user: Uuid, + ) -> Result<(), MetastoreError> { + let now_ms = Utc::now().timestamp_millis(); + let use_value = InviteCodeUseValue { + used_by: used_by_user, + used_at_ms: now_ms, + }; + + let use_key = invite_use_key(code.code(), used_by_user); + let used_by_key = invite_code_used_by_key(used_by_user); + + let mut batch = self.db.batch(); + batch.insert(&self.infra, use_key.as_slice(), use_value.serialize()); + batch.insert(&self.infra, used_by_key.as_slice(), code.code().as_bytes()); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_invite_codes_for_account( + &self, + for_account: &Did, + ) -> Result, MetastoreError> { + let did_str = for_account.to_string(); + let prefix = invite_code_prefix(); + + self.infra + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = InviteCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt invite code"))?; + let matches = val.for_account.as_ref().is_some_and(|d| *d == did_str); + if matches { + acc.push(self.value_to_invite_info(&val)?); + } + Ok(acc) + }) + } + + pub fn get_invite_code_uses(&self, code: &str) -> Result, MetastoreError> { + let prefix = invite_use_prefix(code); + + self.infra + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = InviteCodeUseValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt invite use"))?; + let used_by_did = self + .resolve_did_for_uuid(val.used_by) + .unwrap_or_else(|| Did::new("did:plc:unknown".to_owned()).unwrap()); + let used_by_handle = self.resolve_handle_for_uuid(val.used_by); + acc.push(InviteCodeUse { + code: code.to_owned(), + used_by_did, + used_by_handle, + used_at: DateTime::from_timestamp_millis(val.used_at_ms).unwrap_or_default(), + }); + Ok(acc) + }) + } + + pub fn disable_invite_codes_by_code(&self, codes: &[String]) -> Result<(), MetastoreError> { + let mut batch = self.db.batch(); + + codes.iter().try_for_each(|code| { + let key = invite_code_key(code); + let val: Option = point_lookup( + &self.infra, + key.as_slice(), + InviteCodeValue::deserialize, + "corrupt invite code", + )?; + if let Some(mut v) = val { + v.disabled = true; + batch.insert(&self.infra, key.as_slice(), v.serialize()); + } + Ok::<(), MetastoreError>(()) + })?; + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn disable_invite_codes_by_account(&self, accounts: &[Did]) -> Result<(), MetastoreError> { + let account_strs: Vec = accounts.iter().map(|d| d.to_string()).collect(); + let prefix = invite_code_prefix(); + let mut batch = self.db.batch(); + + self.infra.prefix(prefix.as_slice()).try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let mut val = InviteCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt invite code"))?; + let matches = val + .for_account + .as_ref() + .is_some_and(|d| account_strs.iter().any(|a| a == d)); + if matches { + val.disabled = true; + batch.insert(&self.infra, key_bytes.as_ref(), val.serialize()); + } + Ok::<(), MetastoreError>(()) + })?; + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn list_invite_codes( + &self, + cursor: Option<&str>, + limit: i64, + sort: InviteCodeSortOrder, + ) -> Result, MetastoreError> { + let prefix = invite_code_prefix(); + let limit = usize::try_from(limit).unwrap_or(0); + + let mut rows: Vec = + self.infra + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = InviteCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt invite code"))?; + let created_by_user = val.created_by.unwrap_or(Uuid::nil()); + acc.push(InviteCodeRow { + code: val.code, + available_uses: val.available_uses, + disabled: Some(val.disabled), + created_by_user, + created_at: DateTime::from_timestamp_millis(val.created_at_ms) + .unwrap_or_default(), + }); + Ok::<_, MetastoreError>(acc) + })?; + + match sort { + InviteCodeSortOrder::Recent => rows.sort_by_key(|r| std::cmp::Reverse(r.created_at)), + InviteCodeSortOrder::Usage => rows.sort_by_key(|r| r.available_uses), + } + + let result = match cursor { + Some(c) => rows + .into_iter() + .skip_while(|r| r.code != c) + .skip(1) + .take(limit) + .collect(), + None => rows.into_iter().take(limit).collect(), + }; + + Ok(result) + } + + pub fn get_user_dids_by_ids( + &self, + user_ids: &[Uuid], + ) -> Result, MetastoreError> { + user_ids + .iter() + .filter_map(|&uid| self.resolve_did_for_uuid(uid).map(|did| Ok((uid, did)))) + .collect() + } + + pub fn get_invite_code_uses_batch( + &self, + codes: &[String], + ) -> Result, MetastoreError> { + codes.iter().try_fold(Vec::new(), |mut acc, code| { + let uses = self.get_invite_code_uses(code)?; + acc.extend(uses); + Ok(acc) + }) + } + + pub fn get_invites_created_by_user( + &self, + user_id: Uuid, + ) -> Result, MetastoreError> { + let prefix = invite_code_prefix(); + + self.infra + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = InviteCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt invite code"))?; + if val.created_by == Some(user_id) { + acc.push(self.value_to_invite_info(&val)?); + } + Ok(acc) + }) + } + + pub fn get_invite_code_info( + &self, + code: &str, + ) -> Result, MetastoreError> { + let key = invite_code_key(code); + let val: Option = point_lookup( + &self.infra, + key.as_slice(), + InviteCodeValue::deserialize, + "corrupt invite code", + )?; + val.map(|v| self.value_to_invite_info(&v)).transpose() + } + + pub fn get_invite_codes_by_users( + &self, + user_ids: &[Uuid], + ) -> Result, MetastoreError> { + let prefix = invite_code_prefix(); + + self.infra + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = InviteCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt invite code"))?; + if let Some(uid) = val.created_by.filter(|u| user_ids.contains(u)) { + acc.push((uid, self.value_to_invite_info(&val)?)); + } + Ok(acc) + }) + } + + pub fn get_invite_code_used_by_user( + &self, + user_id: Uuid, + ) -> Result, MetastoreError> { + let key = invite_code_used_by_key(user_id); + match self + .infra + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => String::from_utf8(raw.to_vec()) + .map(Some) + .map_err(|_| MetastoreError::CorruptData("invite code used_by not valid utf8")), + None => Ok(None), + } + } + + pub fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), MetastoreError> { + let used_by_key = invite_code_used_by_key(user_id); + let code = match self + .infra + .get(used_by_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => String::from_utf8(raw.to_vec()) + .map_err(|_| MetastoreError::CorruptData("invite code used_by not valid utf8"))?, + None => return Ok(()), + }; + + let use_key = invite_use_key(&code, user_id); + let mut batch = self.db.batch(); + batch.remove(&self.infra, use_key.as_slice()); + batch.remove(&self.infra, used_by_key.as_slice()); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn delete_invite_codes_by_user(&self, user_id: Uuid) -> Result<(), MetastoreError> { + let prefix = invite_code_prefix(); + let mut batch = self.db.batch(); + + self.infra.prefix(prefix.as_slice()).try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = InviteCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt invite code"))?; + if val.created_by == Some(user_id) { + batch.remove(&self.infra, key_bytes.as_ref()); + let use_pfx = invite_use_prefix(&val.code); + delete_all_by_prefix(&self.infra, &mut batch, use_pfx.as_slice())?; + } + Ok::<(), MetastoreError>(()) + })?; + + let user_key = invite_by_user_key(user_id); + batch.remove(&self.infra, user_key.as_slice()); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn reserve_signing_key( + &self, + did: Option<&Did>, + public_key_did_key: &str, + private_key_bytes: &[u8], + expires_at: DateTime, + ) -> Result { + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = SigningKeyValue { + id, + did: did.map(|d| d.to_string()), + public_key_did_key: public_key_did_key.to_owned(), + private_key_bytes: private_key_bytes.to_vec(), + used: false, + created_at_ms: now_ms, + expires_at_ms: expires_at.timestamp_millis(), + }; + + let primary_key = signing_key_key(public_key_did_key); + let id_index_key = signing_key_by_id_key(id); + + let mut batch = self.db.batch(); + batch.insert(&self.infra, primary_key.as_slice(), value.serialize()); + batch.insert( + &self.infra, + id_index_key.as_slice(), + public_key_did_key.as_bytes(), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(id) + } + + pub fn get_reserved_signing_key( + &self, + public_key_did_key: &str, + ) -> Result, MetastoreError> { + let key = signing_key_key(public_key_did_key); + let val: Option = point_lookup( + &self.infra, + key.as_slice(), + SigningKeyValue::deserialize, + "corrupt signing key", + )?; + Ok(val.map(|v| ReservedSigningKey { + id: v.id, + private_key_bytes: v.private_key_bytes, + })) + } + + pub fn mark_signing_key_used(&self, key_id: Uuid) -> Result<(), MetastoreError> { + let id_key = signing_key_by_id_key(key_id); + let pub_key_str = match self + .infra + .get(id_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => String::from_utf8(raw.to_vec()) + .map_err(|_| MetastoreError::CorruptData("signing key index not valid utf8"))?, + None => return Ok(()), + }; + + let primary_key = signing_key_key(&pub_key_str); + let mut val: SigningKeyValue = point_lookup( + &self.infra, + primary_key.as_slice(), + SigningKeyValue::deserialize, + "corrupt signing key", + )? + .ok_or(MetastoreError::CorruptData( + "signing key missing from primary", + ))?; + + val.used = true; + self.infra + .insert(primary_key.as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn create_deletion_request( + &self, + token: &str, + did: &Did, + expires_at: DateTime, + ) -> Result<(), MetastoreError> { + let now_ms = Utc::now().timestamp_millis(); + let value = DeletionRequestValue { + token: token.to_owned(), + did: did.to_string(), + created_at_ms: now_ms, + expires_at_ms: expires_at.timestamp_millis(), + }; + + let primary_key = deletion_request_key(token); + let did_key = deletion_by_did_key(did.as_str()); + + let mut batch = self.db.batch(); + batch.insert(&self.infra, primary_key.as_slice(), value.serialize()); + batch.insert(&self.infra, did_key.as_slice(), token.as_bytes()); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_deletion_request( + &self, + token: &str, + ) -> Result, MetastoreError> { + let key = deletion_request_key(token); + let val: Option = point_lookup( + &self.infra, + key.as_slice(), + DeletionRequestValue::deserialize, + "corrupt deletion request", + )?; + Ok(val.and_then(|v| { + Did::new(v.did).ok().map(|did| DeletionRequest { + did, + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), + }) + })) + } + + pub fn delete_deletion_request(&self, token: &str) -> Result<(), MetastoreError> { + let primary_key = deletion_request_key(token); + + let val: Option = point_lookup( + &self.infra, + primary_key.as_slice(), + DeletionRequestValue::deserialize, + "corrupt deletion request", + )?; + + let mut batch = self.db.batch(); + batch.remove(&self.infra, primary_key.as_slice()); + + if let Some(v) = val { + let did_key = deletion_by_did_key(&v.did); + batch.remove(&self.infra, did_key.as_slice()); + } + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn delete_deletion_requests_by_did(&self, did: &Did) -> Result<(), MetastoreError> { + let did_key = deletion_by_did_key(did.as_str()); + let token = match self + .infra + .get(did_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => String::from_utf8(raw.to_vec()) + .map_err(|_| MetastoreError::CorruptData("deletion by_did not valid utf8"))?, + None => return Ok(()), + }; + + let primary_key = deletion_request_key(&token); + let mut batch = self.db.batch(); + batch.remove(&self.infra, primary_key.as_slice()); + batch.remove(&self.infra, did_key.as_slice()); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn upsert_account_preference( + &self, + user_id: Uuid, + name: &str, + value_json: serde_json::Value, + ) -> Result<(), MetastoreError> { + let key = account_pref_key(user_id, name); + let bytes = serde_json::to_vec(&value_json) + .map_err(|_| MetastoreError::InvalidInput("invalid json for account preference"))?; + self.infra + .insert(key.as_slice(), bytes) + .map_err(MetastoreError::Fjall) + } + + pub fn insert_account_preference_if_not_exists( + &self, + user_id: Uuid, + name: &str, + value_json: serde_json::Value, + ) -> Result<(), MetastoreError> { + let key = account_pref_key(user_id, name); + let existing = self + .infra + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)?; + if existing.is_some() { + return Ok(()); + } + let bytes = serde_json::to_vec(&value_json) + .map_err(|_| MetastoreError::InvalidInput("invalid json for account preference"))?; + self.infra + .insert(key.as_slice(), bytes) + .map_err(MetastoreError::Fjall) + } + + pub fn get_account_preferences( + &self, + user_id: Uuid, + ) -> Result, MetastoreError> { + let prefix = account_pref_prefix(user_id); + + self.infra + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let mut reader = super::encoding::KeyReader::new(&key_bytes); + reader.tag(); + reader.bytes(); + let name = reader + .string() + .ok_or(MetastoreError::CorruptData("corrupt account pref key"))?; + let value: serde_json::Value = serde_json::from_slice(&val_bytes) + .map_err(|_| MetastoreError::CorruptData("corrupt account pref json"))?; + acc.push((name, value)); + Ok(acc) + }) + } + + pub fn replace_namespace_preferences( + &self, + user_id: Uuid, + namespace: &str, + preferences: Vec<(String, serde_json::Value)>, + ) -> Result<(), MetastoreError> { + let prefix = account_pref_prefix(user_id); + let mut batch = self.db.batch(); + + self.infra.prefix(prefix.as_slice()).try_for_each(|guard| { + let (key_bytes, _) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let mut reader = super::encoding::KeyReader::new(&key_bytes); + reader.tag(); + reader.bytes(); + let name = reader + .string() + .ok_or(MetastoreError::CorruptData("corrupt account pref key"))?; + if name.starts_with(namespace) { + batch.remove(&self.infra, key_bytes.as_ref()); + } + Ok::<(), MetastoreError>(()) + })?; + + preferences.iter().try_for_each(|(name, value)| { + let key = account_pref_key(user_id, name); + let bytes = serde_json::to_vec(value) + .map_err(|_| MetastoreError::InvalidInput("invalid json for account preference"))?; + batch.insert(&self.infra, key.as_slice(), bytes); + Ok::<(), MetastoreError>(()) + })?; + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_server_config(&self, key: &str) -> Result, MetastoreError> { + let k = server_config_key(key); + match self + .infra + .get(k.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => String::from_utf8(raw.to_vec()) + .map(Some) + .map_err(|_| MetastoreError::CorruptData("server config not valid utf8")), + None => Ok(None), + } + } + + pub fn get_server_configs( + &self, + keys: &[&str], + ) -> Result, MetastoreError> { + keys.iter() + .filter_map(|&key| { + let k = server_config_key(key); + match self.infra.get(k.as_slice()) { + Ok(Some(raw)) => String::from_utf8(raw.to_vec()) + .ok() + .map(|v| Ok((key.to_owned(), v))), + Ok(None) => None, + Err(e) => Some(Err(MetastoreError::Fjall(e))), + } + }) + .collect() + } + + pub fn upsert_server_config(&self, key: &str, value: &str) -> Result<(), MetastoreError> { + let k = server_config_key(key); + self.infra + .insert(k.as_slice(), value.as_bytes()) + .map_err(MetastoreError::Fjall) + } + + pub fn delete_server_config(&self, key: &str) -> Result<(), MetastoreError> { + let k = server_config_key(key); + self.infra + .remove(k.as_slice()) + .map_err(MetastoreError::Fjall) + } + + pub fn health_check(&self) -> Result { + Ok(true) + } + + pub fn insert_report( + &self, + id: i64, + reason_type: &str, + reason: Option<&str>, + subject_json: serde_json::Value, + reported_by_did: &Did, + created_at: DateTime, + ) -> Result<(), MetastoreError> { + let value = ReportValue { + id, + reason_type: reason_type.to_owned(), + reason: reason.map(str::to_owned), + subject_json: serde_json::to_vec(&subject_json).unwrap_or_default(), + reported_by_did: reported_by_did.to_string(), + created_at_ms: created_at.timestamp_millis(), + }; + + let key = report_key(id); + self.infra + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn delete_plc_tokens_for_user(&self, user_id: Uuid) -> Result<(), MetastoreError> { + let prefix = plc_token_prefix(user_id); + let mut batch = self.db.batch(); + delete_all_by_prefix(&self.infra, &mut batch, prefix.as_slice())?; + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn insert_plc_token( + &self, + user_id: Uuid, + token: &str, + expires_at: DateTime, + ) -> Result<(), MetastoreError> { + let key = plc_token_key(user_id, token); + let expires_at_ms = expires_at.timestamp_millis(); + self.infra + .insert(key.as_slice(), expires_at_ms.to_be_bytes()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_plc_token_expiry( + &self, + user_id: Uuid, + token: &str, + ) -> Result>, MetastoreError> { + let key = plc_token_key(user_id, token); + match self + .infra + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => { + let arr: [u8; 8] = raw + .as_ref() + .try_into() + .map_err(|_| MetastoreError::CorruptData("plc token expiry not 8 bytes"))?; + let ms = i64::from_be_bytes(arr); + Ok(DateTime::from_timestamp_millis(ms)) + } + None => Ok(None), + } + } + + pub fn delete_plc_token(&self, user_id: Uuid, token: &str) -> Result<(), MetastoreError> { + let key = plc_token_key(user_id, token); + self.infra + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_notification_history( + &self, + user_id: Uuid, + limit: i64, + ) -> Result, MetastoreError> { + let prefix = comms_history_prefix(user_id); + let limit = usize::try_from(limit).unwrap_or(0); + + self.infra + .prefix(prefix.as_slice()) + .take(limit) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = NotificationHistoryValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt notification history"))?; + let channel = u8_to_channel(val.channel) + .ok_or(MetastoreError::CorruptData("invalid history channel"))?; + let comms_type = u8_to_comms_type(val.comms_type) + .ok_or(MetastoreError::CorruptData("invalid history comms type"))?; + let status = u8_to_status(val.status) + .ok_or(MetastoreError::CorruptData("invalid history status"))?; + acc.push(NotificationHistoryRow { + created_at: DateTime::from_timestamp_millis(val.created_at_ms) + .unwrap_or_default(), + channel, + comms_type, + status, + subject: val.subject, + body: val.body, + }); + Ok(acc) + }) + } + + pub fn get_blob_storage_key_by_cid( + &self, + cid: &CidLink, + ) -> Result, MetastoreError> { + let cid_str = cid.as_str(); + let cid_index_key = blob_by_cid_key(cid_str); + let user_hash_raw = match self + .repo_data + .get(cid_index_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => { + let arr: [u8; 8] = raw + .as_ref() + .try_into() + .map_err(|_| MetastoreError::CorruptData("blob_by_cid value not 8 bytes"))?; + u64::from_be_bytes(arr) + } + None => return Ok(None), + }; + let user_hash = UserHash::from_raw(user_hash_raw); + let key = blob_meta_key(user_hash, cid_str); + let val: Option = point_lookup( + &self.repo_data, + key.as_slice(), + BlobMetaValue::deserialize, + "corrupt blob_meta value", + )?; + Ok(val.map(|v| v.storage_key)) + } + + pub fn delete_blob_by_cid(&self, cid: &CidLink) -> Result<(), MetastoreError> { + let cid_str = cid.as_str(); + let cid_index_key = blob_by_cid_key(cid_str); + let user_hash_raw = match self + .repo_data + .get(cid_index_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => { + let arr: [u8; 8] = raw + .as_ref() + .try_into() + .map_err(|_| MetastoreError::CorruptData("blob_by_cid value not 8 bytes"))?; + u64::from_be_bytes(arr) + } + None => return Ok(()), + }; + let user_hash = UserHash::from_raw(user_hash_raw); + let primary_key = blob_meta_key(user_hash, cid_str); + + let mut batch = self.db.batch(); + batch.remove(&self.repo_data, primary_key.as_slice()); + batch.remove(&self.repo_data, cid_index_key.as_slice()); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_admin_account_info_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = UserHash::from_did(did.as_str()); + self.resolve_user_value(user_hash) + .map(|u| self.user_to_admin_info(&u)) + .transpose() + } + + pub fn get_admin_account_infos_by_dids( + &self, + dids: &[Did], + ) -> Result, MetastoreError> { + dids.iter() + .filter_map(|did| { + let user_hash = UserHash::from_did(did.as_str()); + self.resolve_user_value(user_hash) + .map(|u| self.user_to_admin_info(&u)) + }) + .collect() + } + + pub fn get_invite_code_uses_by_users( + &self, + user_ids: &[Uuid], + ) -> Result, MetastoreError> { + user_ids + .iter() + .filter_map(|&uid| { + let key = invite_code_used_by_key(uid); + match self.infra.get(key.as_slice()) { + Ok(Some(raw)) => String::from_utf8(raw.to_vec()) + .ok() + .map(|code| Ok((uid, code))), + Ok(None) => None, + Err(e) => Some(Err(MetastoreError::Fjall(e))), + } + }) + .collect() + } +} diff --git a/crates/tranquil-store/src/metastore/infra_schema.rs b/crates/tranquil-store/src/metastore/infra_schema.rs new file mode 100644 index 0000000..ba2f2d7 --- /dev/null +++ b/crates/tranquil-store/src/metastore/infra_schema.rs @@ -0,0 +1,730 @@ +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +use super::encoding::KeyBuilder; +use super::keys::KeyTag; + +const COMMS_SCHEMA_VERSION: u8 = 1; +const INVITE_CODE_SCHEMA_VERSION: u8 = 1; +const INVITE_USE_SCHEMA_VERSION: u8 = 1; +const SIGNING_KEY_SCHEMA_VERSION: u8 = 1; +const DELETION_REQUEST_SCHEMA_VERSION: u8 = 1; +const REPORT_SCHEMA_VERSION: u8 = 1; +const NOTIFICATION_HISTORY_SCHEMA_VERSION: u8 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct QueuedCommsValue { + pub id: uuid::Uuid, + pub user_id: Option, + pub channel: u8, + pub comms_type: u8, + pub recipient: String, + pub subject: Option, + pub body: String, + pub metadata: Option>, + pub status: u8, + pub error_message: Option, + pub attempts: i32, + pub max_attempts: i32, + pub created_at_ms: i64, + pub scheduled_for_ms: i64, + pub sent_at_ms: Option, +} + +impl QueuedCommsValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("QueuedCommsValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(COMMS_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + COMMS_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InviteCodeValue { + pub code: String, + pub available_uses: i32, + pub disabled: bool, + pub for_account: Option, + pub created_by: Option, + pub created_at_ms: i64, +} + +impl InviteCodeValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("InviteCodeValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(INVITE_CODE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + INVITE_CODE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InviteCodeUseValue { + pub used_by: uuid::Uuid, + pub used_at_ms: i64, +} + +impl InviteCodeUseValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("InviteCodeUseValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(INVITE_USE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + INVITE_USE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SigningKeyValue { + pub id: uuid::Uuid, + pub did: Option, + pub public_key_did_key: String, + pub private_key_bytes: Vec, + pub used: bool, + pub created_at_ms: i64, + pub expires_at_ms: i64, +} + +impl SigningKeyValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("SigningKeyValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(SIGNING_KEY_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + SIGNING_KEY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DeletionRequestValue { + pub token: String, + pub did: String, + pub created_at_ms: i64, + pub expires_at_ms: i64, +} + +impl DeletionRequestValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("DeletionRequestValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(DELETION_REQUEST_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + DELETION_REQUEST_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ReportValue { + pub id: i64, + pub reason_type: String, + pub reason: Option, + pub subject_json: Vec, + pub reported_by_did: String, + pub created_at_ms: i64, +} + +impl ReportValue { + pub fn serialize(&self) -> Vec { + let payload = postcard::to_allocvec(self).expect("ReportValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(REPORT_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + REPORT_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NotificationHistoryValue { + pub id: uuid::Uuid, + pub channel: u8, + pub comms_type: u8, + pub recipient: String, + pub subject: Option, + pub body: String, + pub status: u8, + pub created_at_ms: i64, +} + +impl NotificationHistoryValue { + pub fn serialize(&self) -> Vec { + let payload = postcard::to_allocvec(self) + .expect("NotificationHistoryValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(NOTIFICATION_HISTORY_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + NOTIFICATION_HISTORY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +pub fn channel_to_u8(ch: tranquil_db_traits::CommsChannel) -> u8 { + match ch { + tranquil_db_traits::CommsChannel::Email => 0, + tranquil_db_traits::CommsChannel::Discord => 1, + tranquil_db_traits::CommsChannel::Telegram => 2, + tranquil_db_traits::CommsChannel::Signal => 3, + } +} + +pub fn u8_to_channel(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::CommsChannel::Email), + 1 => Some(tranquil_db_traits::CommsChannel::Discord), + 2 => Some(tranquil_db_traits::CommsChannel::Telegram), + 3 => Some(tranquil_db_traits::CommsChannel::Signal), + _ => None, + } +} + +pub fn comms_type_to_u8(ct: tranquil_db_traits::CommsType) -> u8 { + match ct { + tranquil_db_traits::CommsType::Welcome => 0, + tranquil_db_traits::CommsType::EmailVerification => 1, + tranquil_db_traits::CommsType::PasswordReset => 2, + tranquil_db_traits::CommsType::EmailUpdate => 3, + tranquil_db_traits::CommsType::AccountDeletion => 4, + tranquil_db_traits::CommsType::AdminEmail => 5, + tranquil_db_traits::CommsType::PlcOperation => 6, + tranquil_db_traits::CommsType::TwoFactorCode => 7, + tranquil_db_traits::CommsType::PasskeyRecovery => 8, + tranquil_db_traits::CommsType::LegacyLoginAlert => 9, + tranquil_db_traits::CommsType::MigrationVerification => 10, + tranquil_db_traits::CommsType::ChannelVerification => 11, + tranquil_db_traits::CommsType::ChannelVerified => 12, + } +} + +pub fn u8_to_comms_type(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::CommsType::Welcome), + 1 => Some(tranquil_db_traits::CommsType::EmailVerification), + 2 => Some(tranquil_db_traits::CommsType::PasswordReset), + 3 => Some(tranquil_db_traits::CommsType::EmailUpdate), + 4 => Some(tranquil_db_traits::CommsType::AccountDeletion), + 5 => Some(tranquil_db_traits::CommsType::AdminEmail), + 6 => Some(tranquil_db_traits::CommsType::PlcOperation), + 7 => Some(tranquil_db_traits::CommsType::TwoFactorCode), + 8 => Some(tranquil_db_traits::CommsType::PasskeyRecovery), + 9 => Some(tranquil_db_traits::CommsType::LegacyLoginAlert), + 10 => Some(tranquil_db_traits::CommsType::MigrationVerification), + 11 => Some(tranquil_db_traits::CommsType::ChannelVerification), + 12 => Some(tranquil_db_traits::CommsType::ChannelVerified), + _ => None, + } +} + +pub fn status_to_u8(s: tranquil_db_traits::CommsStatus) -> u8 { + match s { + tranquil_db_traits::CommsStatus::Pending => 0, + tranquil_db_traits::CommsStatus::Processing => 1, + tranquil_db_traits::CommsStatus::Sent => 2, + tranquil_db_traits::CommsStatus::Failed => 3, + } +} + +pub fn u8_to_status(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::CommsStatus::Pending), + 1 => Some(tranquil_db_traits::CommsStatus::Processing), + 2 => Some(tranquil_db_traits::CommsStatus::Sent), + 3 => Some(tranquil_db_traits::CommsStatus::Failed), + _ => None, + } +} + +pub fn comms_queue_key(id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_COMMS_QUEUE) + .bytes(id.as_bytes()) + .build() +} + +pub fn comms_queue_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::INFRA_COMMS_QUEUE).build() +} + +pub fn invite_code_key(code: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_INVITE_CODE) + .string(code) + .build() +} + +pub fn invite_code_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::INFRA_INVITE_CODE).build() +} + +pub fn invite_use_key(code: &str, used_by: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_INVITE_USE) + .string(code) + .bytes(used_by.as_bytes()) + .build() +} + +pub fn invite_use_prefix(code: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_INVITE_USE) + .string(code) + .build() +} + +pub fn invite_by_account_key(did: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_INVITE_BY_ACCOUNT) + .string(did) + .build() +} + +pub fn invite_by_user_key(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_INVITE_BY_USER) + .bytes(user_id.as_bytes()) + .build() +} + +pub fn invite_by_user_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::INFRA_INVITE_BY_USER).build() +} + +pub fn signing_key_key(public_key_did_key: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_SIGNING_KEY) + .string(public_key_did_key) + .build() +} + +pub fn signing_key_by_id_key(key_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_SIGNING_KEY_BY_ID) + .bytes(key_id.as_bytes()) + .build() +} + +pub fn deletion_request_key(token: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_DELETION_REQUEST) + .string(token) + .build() +} + +pub fn deletion_by_did_key(did: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_DELETION_BY_DID) + .string(did) + .build() +} + +pub fn deletion_by_did_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::INFRA_DELETION_BY_DID).build() +} + +pub fn account_pref_key(user_id: uuid::Uuid, name: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_ACCOUNT_PREF) + .bytes(user_id.as_bytes()) + .string(name) + .build() +} + +pub fn account_pref_prefix(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_ACCOUNT_PREF) + .bytes(user_id.as_bytes()) + .build() +} + +pub fn server_config_key(key: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_SERVER_CONFIG) + .string(key) + .build() +} + +pub fn report_key(id: i64) -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::INFRA_REPORT).i64(id).build() +} + +pub fn plc_token_key(user_id: uuid::Uuid, token: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_PLC_TOKEN) + .bytes(user_id.as_bytes()) + .string(token) + .build() +} + +pub fn plc_token_prefix(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_PLC_TOKEN) + .bytes(user_id.as_bytes()) + .build() +} + +pub fn comms_history_key( + user_id: uuid::Uuid, + created_at_ms: i64, + id: uuid::Uuid, +) -> SmallVec<[u8; 128]> { + let reversed_ts = i64::MAX.saturating_sub(created_at_ms); + KeyBuilder::new() + .tag(KeyTag::INFRA_COMMS_HISTORY) + .bytes(user_id.as_bytes()) + .i64(reversed_ts) + .bytes(id.as_bytes()) + .build() +} + +pub fn comms_history_prefix(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_COMMS_HISTORY) + .bytes(user_id.as_bytes()) + .build() +} + +pub fn invite_code_used_by_key(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::INFRA_INVITE_CODE_USED_BY) + .bytes(user_id.as_bytes()) + .build() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metastore::encoding::KeyReader; + + #[test] + fn queued_comms_value_roundtrip() { + let val = QueuedCommsValue { + id: uuid::Uuid::new_v4(), + user_id: Some(uuid::Uuid::new_v4()), + channel: 0, + comms_type: 1, + recipient: "user@example.com".to_owned(), + subject: Some("test".to_owned()), + body: "body text".to_owned(), + metadata: None, + status: 0, + error_message: None, + attempts: 0, + max_attempts: 3, + created_at_ms: 1700000000000, + scheduled_for_ms: 1700000000000, + sent_at_ms: None, + }; + let bytes = val.serialize(); + assert_eq!(bytes[0], COMMS_SCHEMA_VERSION); + let decoded = QueuedCommsValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn invite_code_value_roundtrip() { + let val = InviteCodeValue { + code: "abc-def-ghi".to_owned(), + available_uses: 5, + disabled: false, + for_account: Some("did:plc:test".to_owned()), + created_by: Some(uuid::Uuid::new_v4()), + created_at_ms: 1700000000000, + }; + let bytes = val.serialize(); + assert_eq!(bytes[0], INVITE_CODE_SCHEMA_VERSION); + let decoded = InviteCodeValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn invite_use_value_roundtrip() { + let val = InviteCodeUseValue { + used_by: uuid::Uuid::new_v4(), + used_at_ms: 1700000000000, + }; + let bytes = val.serialize(); + let decoded = InviteCodeUseValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn signing_key_value_roundtrip() { + let val = SigningKeyValue { + id: uuid::Uuid::new_v4(), + did: Some("did:plc:test".to_owned()), + public_key_did_key: "did:key:z123".to_owned(), + private_key_bytes: vec![1, 2, 3, 4], + used: false, + created_at_ms: 1700000000000, + expires_at_ms: 1700000600000, + }; + let bytes = val.serialize(); + let decoded = SigningKeyValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn deletion_request_value_roundtrip() { + let val = DeletionRequestValue { + token: "tok-abc".to_owned(), + did: "did:plc:test".to_owned(), + created_at_ms: 1700000000000, + expires_at_ms: 1700000600000, + }; + let bytes = val.serialize(); + let decoded = DeletionRequestValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn report_value_roundtrip() { + let val = ReportValue { + id: 42, + reason_type: "spam".to_owned(), + reason: Some("bad content".to_owned()), + subject_json: b"{}".to_vec(), + reported_by_did: "did:plc:reporter".to_owned(), + created_at_ms: 1700000000000, + }; + let bytes = val.serialize(); + let decoded = ReportValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn notification_history_value_roundtrip() { + let val = NotificationHistoryValue { + id: uuid::Uuid::new_v4(), + channel: 0, + comms_type: 1, + recipient: "user@example.com".to_owned(), + subject: None, + body: "notification body".to_owned(), + status: 2, + created_at_ms: 1700000000000, + }; + let bytes = val.serialize(); + let decoded = NotificationHistoryValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn comms_queue_key_roundtrip() { + let id = uuid::Uuid::new_v4(); + let key = comms_queue_key(id); + let mut reader = KeyReader::new(&key); + assert_eq!(reader.tag(), Some(KeyTag::INFRA_COMMS_QUEUE.raw())); + let id_bytes = reader.bytes().unwrap(); + assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), id); + assert!(reader.is_empty()); + } + + #[test] + fn invite_code_key_roundtrip() { + let key = invite_code_key("abc-def"); + let mut reader = KeyReader::new(&key); + assert_eq!(reader.tag(), Some(KeyTag::INFRA_INVITE_CODE.raw())); + assert_eq!(reader.string(), Some("abc-def".to_owned())); + assert!(reader.is_empty()); + } + + #[test] + fn invite_use_key_roundtrip() { + let user_id = uuid::Uuid::new_v4(); + let key = invite_use_key("code1", user_id); + let mut reader = KeyReader::new(&key); + assert_eq!(reader.tag(), Some(KeyTag::INFRA_INVITE_USE.raw())); + assert_eq!(reader.string(), Some("code1".to_owned())); + let id_bytes = reader.bytes().unwrap(); + assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), user_id); + assert!(reader.is_empty()); + } + + #[test] + fn comms_history_newest_first_ordering() { + let user_id = uuid::Uuid::new_v4(); + let id_a = uuid::Uuid::new_v4(); + let id_b = uuid::Uuid::new_v4(); + let key_old = comms_history_key(user_id, 1000, id_a); + let key_new = comms_history_key(user_id, 2000, id_b); + assert!(key_new.as_slice() < key_old.as_slice()); + } + + #[test] + fn account_pref_key_roundtrip() { + let user_id = uuid::Uuid::new_v4(); + let key = account_pref_key(user_id, "app.bsky.actor.profile"); + let mut reader = KeyReader::new(&key); + assert_eq!(reader.tag(), Some(KeyTag::INFRA_ACCOUNT_PREF.raw())); + let id_bytes = reader.bytes().unwrap(); + assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), user_id); + assert_eq!(reader.string(), Some("app.bsky.actor.profile".to_owned())); + assert!(reader.is_empty()); + } + + #[test] + fn plc_token_key_roundtrip() { + let user_id = uuid::Uuid::new_v4(); + let key = plc_token_key(user_id, "tok123"); + let mut reader = KeyReader::new(&key); + assert_eq!(reader.tag(), Some(KeyTag::INFRA_PLC_TOKEN.raw())); + let id_bytes = reader.bytes().unwrap(); + assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), user_id); + assert_eq!(reader.string(), Some("tok123".to_owned())); + assert!(reader.is_empty()); + } + + #[test] + fn deserialize_unknown_version_returns_none() { + let val = QueuedCommsValue { + id: uuid::Uuid::new_v4(), + user_id: None, + channel: 0, + comms_type: 0, + recipient: String::new(), + subject: None, + body: String::new(), + metadata: None, + status: 0, + error_message: None, + attempts: 0, + max_attempts: 3, + created_at_ms: 0, + scheduled_for_ms: 0, + sent_at_ms: None, + }; + let mut bytes = val.serialize(); + bytes[0] = 99; + assert!(QueuedCommsValue::deserialize(&bytes).is_none()); + } + + #[test] + fn channel_u8_roundtrip() { + use tranquil_db_traits::CommsChannel; + [ + CommsChannel::Email, + CommsChannel::Discord, + CommsChannel::Telegram, + CommsChannel::Signal, + ] + .iter() + .for_each(|&ch| { + assert_eq!(u8_to_channel(channel_to_u8(ch)), Some(ch)); + }); + } + + #[test] + fn comms_type_u8_roundtrip() { + use tranquil_db_traits::CommsType; + [ + CommsType::Welcome, + CommsType::EmailVerification, + CommsType::PasswordReset, + CommsType::EmailUpdate, + CommsType::AccountDeletion, + CommsType::AdminEmail, + CommsType::PlcOperation, + CommsType::TwoFactorCode, + CommsType::PasskeyRecovery, + CommsType::LegacyLoginAlert, + CommsType::MigrationVerification, + CommsType::ChannelVerification, + CommsType::ChannelVerified, + ] + .iter() + .for_each(|&ct| { + assert_eq!(u8_to_comms_type(comms_type_to_u8(ct)), Some(ct)); + }); + } + + #[test] + fn status_u8_roundtrip() { + use tranquil_db_traits::CommsStatus; + [ + CommsStatus::Pending, + CommsStatus::Processing, + CommsStatus::Sent, + CommsStatus::Failed, + ] + .iter() + .for_each(|&s| { + assert_eq!(u8_to_status(status_to_u8(s)), Some(s)); + }); + } + + #[test] + fn u8_to_channel_invalid_returns_none() { + assert!(u8_to_channel(255).is_none()); + } + + #[test] + fn u8_to_comms_type_invalid_returns_none() { + assert!(u8_to_comms_type(255).is_none()); + } + + #[test] + fn u8_to_status_invalid_returns_none() { + assert!(u8_to_status(255).is_none()); + } +} diff --git a/crates/tranquil-store/src/metastore/keys.rs b/crates/tranquil-store/src/metastore/keys.rs index c43bd31..2398336 100644 --- a/crates/tranquil-store/src/metastore/keys.rs +++ b/crates/tranquil-store/src/metastore/keys.rs @@ -55,6 +55,76 @@ impl KeyTag { pub const RECORD_BLOBS: Self = Self(0x30); pub const BACKLINK_BY_USER: Self = Self(0x31); + pub const USER_PRIMARY: Self = Self(0x40); + pub const USER_BY_HANDLE: Self = Self(0x41); + pub const USER_BY_EMAIL: Self = Self(0x42); + pub const USER_PASSKEYS: Self = Self(0x43); + pub const USER_PASSKEY_BY_CRED: Self = Self(0x44); + pub const USER_TOTP: Self = Self(0x45); + pub const USER_BACKUP_CODES: Self = Self(0x46); + pub const USER_WEBAUTHN_CHALLENGE: Self = Self(0x47); + pub const USER_RESET_CODE: Self = Self(0x48); + pub const USER_RECOVERY_TOKEN: Self = Self(0x49); + pub const USER_DID_WEB_OVERRIDES: Self = Self(0x4A); + pub const USER_HANDLE_RESERVATION: Self = Self(0x4B); + pub const USER_COMMS_CHANNEL: Self = Self(0x4C); + pub const USER_TELEGRAM_LOOKUP: Self = Self(0x4D); + pub const USER_DISCORD_LOOKUP: Self = Self(0x4E); + + pub const SESSION_PRIMARY: Self = Self(0x50); + pub const SESSION_BY_ACCESS: Self = Self(0x51); + pub const SESSION_BY_REFRESH: Self = Self(0x52); + pub const SESSION_USED_REFRESH: Self = Self(0x53); + pub const SESSION_APP_PASSWORD: Self = Self(0x54); + pub const SESSION_BY_DID: Self = Self(0x55); + pub const SESSION_LAST_REAUTH: Self = Self(0x56); + pub const SESSION_ID_COUNTER: Self = Self(0x57); + + pub const OAUTH_TOKEN: Self = Self(0x60); + pub const OAUTH_TOKEN_BY_ID: Self = Self(0x61); + pub const OAUTH_TOKEN_BY_REFRESH: Self = Self(0x62); + pub const OAUTH_TOKEN_BY_PREV_REFRESH: Self = Self(0x63); + pub const OAUTH_USED_REFRESH: Self = Self(0x64); + pub const OAUTH_AUTH_REQUEST: Self = Self(0x65); + pub const OAUTH_AUTH_BY_CODE: Self = Self(0x66); + pub const OAUTH_DEVICE: Self = Self(0x67); + pub const OAUTH_ACCOUNT_DEVICE: Self = Self(0x68); + pub const OAUTH_DPOP_JTI: Self = Self(0x69); + pub const OAUTH_2FA_CHALLENGE: Self = Self(0x6A); + pub const OAUTH_2FA_BY_REQUEST: Self = Self(0x6B); + pub const OAUTH_SCOPE_PREFS: Self = Self(0x6C); + pub const OAUTH_AUTH_CLIENT: Self = Self(0x6D); + pub const OAUTH_DEVICE_TRUST: Self = Self(0x6E); + pub const OAUTH_TOKEN_FAMILY_COUNTER: Self = Self(0x6F); + + pub const INFRA_COMMS_QUEUE: Self = Self(0x70); + pub const INFRA_INVITE_CODE: Self = Self(0x71); + pub const INFRA_INVITE_USE: Self = Self(0x72); + pub const INFRA_INVITE_BY_ACCOUNT: Self = Self(0x73); + pub const INFRA_INVITE_BY_USER: Self = Self(0x74); + pub const INFRA_SIGNING_KEY: Self = Self(0x75); + pub const INFRA_SIGNING_KEY_BY_ID: Self = Self(0x76); + pub const INFRA_DELETION_REQUEST: Self = Self(0x77); + pub const INFRA_DELETION_BY_DID: Self = Self(0x78); + pub const INFRA_ACCOUNT_PREF: Self = Self(0x79); + pub const INFRA_SERVER_CONFIG: Self = Self(0x7A); + pub const INFRA_REPORT: Self = Self(0x7B); + pub const INFRA_PLC_TOKEN: Self = Self(0x7C); + pub const INFRA_COMMS_HISTORY: Self = Self(0x7D); + pub const INFRA_INVITE_CODE_USED_BY: Self = Self(0x7E); + + pub const DELEG_GRANT: Self = Self(0x80); + pub const DELEG_BY_CONTROLLER: Self = Self(0x81); + pub const DELEG_AUDIT_LOG: Self = Self(0x82); + + pub const SSO_IDENTITY: Self = Self(0x90); + pub const SSO_BY_PROVIDER: Self = Self(0x91); + pub const SSO_AUTH_STATE: Self = Self(0x92); + pub const SSO_PENDING_REG: Self = Self(0x93); + pub const SSO_BY_ID: Self = Self(0x94); + + pub const OAUTH_TOKEN_BY_FAMILY: Self = Self(0xA0); + pub const FORMAT_VERSION: Self = Self(0xFF); pub const fn raw(self) -> u8 { @@ -117,6 +187,69 @@ mod tests { KeyTag::DID_EVENTS, KeyTag::RECORD_BLOBS, KeyTag::BACKLINK_BY_USER, + KeyTag::USER_PRIMARY, + KeyTag::USER_BY_HANDLE, + KeyTag::USER_BY_EMAIL, + KeyTag::USER_PASSKEYS, + KeyTag::USER_PASSKEY_BY_CRED, + KeyTag::USER_TOTP, + KeyTag::USER_BACKUP_CODES, + KeyTag::USER_WEBAUTHN_CHALLENGE, + KeyTag::USER_RESET_CODE, + KeyTag::USER_RECOVERY_TOKEN, + KeyTag::USER_DID_WEB_OVERRIDES, + KeyTag::USER_HANDLE_RESERVATION, + KeyTag::USER_COMMS_CHANNEL, + KeyTag::USER_TELEGRAM_LOOKUP, + KeyTag::USER_DISCORD_LOOKUP, + KeyTag::SESSION_PRIMARY, + KeyTag::SESSION_BY_ACCESS, + KeyTag::SESSION_BY_REFRESH, + KeyTag::SESSION_USED_REFRESH, + KeyTag::SESSION_APP_PASSWORD, + KeyTag::SESSION_BY_DID, + KeyTag::SESSION_LAST_REAUTH, + KeyTag::SESSION_ID_COUNTER, + KeyTag::OAUTH_TOKEN, + KeyTag::OAUTH_TOKEN_BY_ID, + KeyTag::OAUTH_TOKEN_BY_REFRESH, + KeyTag::OAUTH_TOKEN_BY_PREV_REFRESH, + KeyTag::OAUTH_USED_REFRESH, + KeyTag::OAUTH_AUTH_REQUEST, + KeyTag::OAUTH_AUTH_BY_CODE, + KeyTag::OAUTH_DEVICE, + KeyTag::OAUTH_ACCOUNT_DEVICE, + KeyTag::OAUTH_DPOP_JTI, + KeyTag::OAUTH_2FA_CHALLENGE, + KeyTag::OAUTH_2FA_BY_REQUEST, + KeyTag::OAUTH_SCOPE_PREFS, + KeyTag::OAUTH_AUTH_CLIENT, + KeyTag::OAUTH_DEVICE_TRUST, + KeyTag::OAUTH_TOKEN_FAMILY_COUNTER, + KeyTag::INFRA_COMMS_QUEUE, + KeyTag::INFRA_INVITE_CODE, + KeyTag::INFRA_INVITE_USE, + KeyTag::INFRA_INVITE_BY_ACCOUNT, + KeyTag::INFRA_INVITE_BY_USER, + KeyTag::INFRA_SIGNING_KEY, + KeyTag::INFRA_SIGNING_KEY_BY_ID, + KeyTag::INFRA_DELETION_REQUEST, + KeyTag::INFRA_DELETION_BY_DID, + KeyTag::INFRA_ACCOUNT_PREF, + KeyTag::INFRA_SERVER_CONFIG, + KeyTag::INFRA_REPORT, + KeyTag::INFRA_PLC_TOKEN, + KeyTag::INFRA_COMMS_HISTORY, + KeyTag::INFRA_INVITE_CODE_USED_BY, + KeyTag::DELEG_GRANT, + KeyTag::DELEG_BY_CONTROLLER, + KeyTag::DELEG_AUDIT_LOG, + KeyTag::SSO_IDENTITY, + KeyTag::SSO_BY_PROVIDER, + KeyTag::SSO_AUTH_STATE, + KeyTag::SSO_PENDING_REG, + KeyTag::SSO_BY_ID, + KeyTag::OAUTH_TOKEN_BY_FAMILY, KeyTag::FORMAT_VERSION, ]; let mut raw: Vec = tags.iter().map(|t| t.raw()).collect(); diff --git a/crates/tranquil-store/src/metastore/mod.rs b/crates/tranquil-store/src/metastore/mod.rs index ad4e6fb..84e1fa0 100644 --- a/crates/tranquil-store/src/metastore/mod.rs +++ b/crates/tranquil-store/src/metastore/mod.rs @@ -3,10 +3,16 @@ pub mod backlinks; pub mod blob_ops; pub mod blobs; pub mod commit_ops; +pub mod delegation_ops; +pub mod delegations; pub mod encoding; pub mod event_keys; pub mod event_ops; +pub mod infra_ops; +pub mod infra_schema; pub mod keys; +pub mod oauth_ops; +pub mod oauth_schema; pub mod partitions; pub mod record_ops; pub mod records; @@ -14,9 +20,15 @@ pub mod recovery; pub mod repo_meta; pub mod repo_ops; pub mod scan; +pub mod session_ops; +pub mod sessions; +pub mod sso_ops; +pub mod sso_schema; pub mod user_block_ops; pub mod user_blocks; pub mod user_hash; +pub mod user_ops; +pub mod users; use std::path::Path; use std::sync::Arc; @@ -145,6 +157,7 @@ pub struct Metastore { db: Database, partitions: [Keyspace; Partition::ALL.len()], user_hashes: Arc, + counter_lock: Arc>, } impl Metastore { @@ -185,6 +198,7 @@ impl Metastore { db, partitions, user_hashes, + counter_lock: Arc::new(parking_lot::Mutex::new(())), }) } @@ -274,6 +288,61 @@ impl Metastore { ) } + pub fn delegation_ops(&self) -> delegation_ops::DelegationOps { + delegation_ops::DelegationOps::new( + self.db.clone(), + self.partitions[Partition::Indexes.index()].clone(), + self.partitions[Partition::Users.index()].clone(), + Arc::clone(&self.user_hashes), + ) + } + + pub fn sso_ops(&self) -> sso_ops::SsoOps { + sso_ops::SsoOps::new( + self.db.clone(), + self.partitions[Partition::Indexes.index()].clone(), + ) + } + + pub fn session_ops(&self) -> session_ops::SessionOps { + session_ops::SessionOps::new( + self.db.clone(), + self.partitions[Partition::Auth.index()].clone(), + self.partitions[Partition::Users.index()].clone(), + Arc::clone(&self.user_hashes), + Arc::clone(&self.counter_lock), + ) + } + + pub fn infra_ops(&self) -> infra_ops::InfraOps { + infra_ops::InfraOps::new( + self.db.clone(), + self.partitions[Partition::Infra.index()].clone(), + self.partitions[Partition::RepoData.index()].clone(), + self.partitions[Partition::Users.index()].clone(), + Arc::clone(&self.user_hashes), + ) + } + + pub fn oauth_ops(&self) -> oauth_ops::OAuthOps { + oauth_ops::OAuthOps::new( + self.db.clone(), + self.partitions[Partition::Auth.index()].clone(), + self.partitions[Partition::Users.index()].clone(), + Arc::clone(&self.counter_lock), + ) + } + + pub fn user_ops(&self) -> user_ops::UserOps { + user_ops::UserOps::new( + self.db.clone(), + self.partitions[Partition::Users.index()].clone(), + self.partitions[Partition::RepoData.index()].clone(), + self.partitions[Partition::Auth.index()].clone(), + Arc::clone(&self.user_hashes), + ) + } + pub fn commit_ops( &self, bridge: Arc>, diff --git a/crates/tranquil-store/src/metastore/oauth_ops.rs b/crates/tranquil-store/src/metastore/oauth_ops.rs new file mode 100644 index 0000000..16e9145 --- /dev/null +++ b/crates/tranquil-store/src/metastore/oauth_ops.rs @@ -0,0 +1,1602 @@ +use std::sync::Arc; + +use chrono::{DateTime, Duration, Utc}; +use fjall::{Database, Keyspace}; +use uuid::Uuid; + +use super::MetastoreError; +use super::keys::UserHash; +use super::oauth_schema::{ + AccountDeviceValue, AuthorizedClientValue, DeviceTrustValue, DpopJtiValue, OAuthDeviceValue, + OAuthRequestValue, OAuthTokenValue, ScopePrefsValue, TokenIndexValue, TwoFactorChallengeValue, + UsedRefreshValue, deserialize_family_counter, oauth_2fa_by_request_key, + oauth_2fa_challenge_key, oauth_2fa_challenge_prefix, oauth_account_device_key, + oauth_auth_by_code_key, oauth_auth_client_key, oauth_auth_request_key, + oauth_auth_request_prefix, oauth_device_key, oauth_device_trust_key, oauth_device_trust_prefix, + oauth_dpop_jti_key, oauth_dpop_jti_prefix, oauth_scope_prefs_key, oauth_token_by_family_key, + oauth_token_by_id_key, oauth_token_by_prev_refresh_key, oauth_token_by_refresh_key, + oauth_token_family_counter_key, oauth_token_key, oauth_token_user_prefix, + oauth_used_refresh_key, serialize_family_counter, +}; +use super::scan::point_lookup; +use super::users::UserValue; + +use tranquil_db_traits::{ + DeviceAccountRow, DeviceTrustInfo, OAuthSessionListItem, ScopePreference, TokenFamilyId, + TrustedDeviceRow, TwoFactorChallenge, +}; +use tranquil_oauth::{AuthorizedClientData, DeviceData, RequestData, TokenData}; +use tranquil_types::{ + AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, + TokenId, +}; + +pub struct OAuthOps { + db: Database, + auth: Keyspace, + users: Keyspace, + counter_lock: Arc>, +} + +impl OAuthOps { + pub fn new( + db: Database, + auth: Keyspace, + users: Keyspace, + counter_lock: Arc>, + ) -> Self { + Self { + db, + auth, + users, + counter_lock, + } + } + + fn resolve_user_hash_from_did(&self, did: &str) -> UserHash { + UserHash::from_did(did) + } + + fn load_user_value(&self, user_hash: UserHash) -> Result, MetastoreError> { + let key = super::encoding::KeyBuilder::new() + .tag(super::keys::KeyTag::USER_PRIMARY) + .u64(user_hash.raw()) + .build(); + point_lookup( + &self.users, + key.as_slice(), + UserValue::deserialize, + "corrupt user value", + ) + } + + fn next_family_id(&self) -> Result { + let _guard = self.counter_lock.lock(); + let counter_key = oauth_token_family_counter_key(); + let current = self + .auth + .get(counter_key.as_slice()) + .map_err(MetastoreError::Fjall)? + .and_then(|raw| deserialize_family_counter(&raw)) + .unwrap_or(0); + let next = current.saturating_add(1); + self.auth + .insert(counter_key.as_slice(), serialize_family_counter(next)) + .map_err(MetastoreError::Fjall)?; + Ok(next) + } + + fn load_token_by_family_id( + &self, + user_hash: UserHash, + family_id: i32, + ) -> Result, MetastoreError> { + let key = oauth_token_key(user_hash, family_id); + point_lookup( + &self.auth, + key.as_slice(), + OAuthTokenValue::deserialize, + "corrupt oauth token", + ) + } + + fn token_value_to_data(&self, v: &OAuthTokenValue) -> Result { + let did = Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid did in oauth token"))?; + let token_id = tranquil_oauth::TokenId(v.token_id.clone()); + let refresh_token = if v.refresh_token.is_empty() { + None + } else { + Some(tranquil_oauth::RefreshToken(v.refresh_token.clone())) + }; + + Ok(TokenData { + did, + token_id, + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + updated_at: DateTime::from_timestamp_millis(v.updated_at_ms).unwrap_or_default(), + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), + client_id: v.client_id.clone(), + client_auth: tranquil_oauth::ClientAuth::None, + device_id: None, + parameters: serde_json::from_str(&v.parameters_json) + .unwrap_or_else(|_| default_parameters(&v.client_id)), + details: None, + code: None, + current_refresh_token: refresh_token, + scope: Some(v.scope.clone()).filter(|s| !s.is_empty()), + controller_did: v + .controller_did + .as_ref() + .and_then(|d| Did::new(d.clone()).ok()), + }) + } + + fn delete_token_indexes( + &self, + batch: &mut fjall::OwnedWriteBatch, + token: &OAuthTokenValue, + user_hash: UserHash, + ) { + batch.remove( + &self.auth, + oauth_token_key(user_hash, token.family_id).as_slice(), + ); + batch.remove( + &self.auth, + oauth_token_by_id_key(&token.token_id).as_slice(), + ); + batch.remove( + &self.auth, + oauth_token_by_refresh_key(&token.refresh_token).as_slice(), + ); + if let Some(prev) = &token.previous_refresh_token { + batch.remove(&self.auth, oauth_token_by_prev_refresh_key(prev).as_slice()); + } + batch.remove( + &self.auth, + oauth_token_by_family_key(token.family_id).as_slice(), + ); + } + + fn collect_tokens_for_did( + &self, + user_hash: UserHash, + ) -> Result, MetastoreError> { + let prefix = oauth_token_user_prefix(user_hash); + self.auth + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + match OAuthTokenValue::deserialize(&val_bytes) { + Some(v) => { + acc.push(v); + Ok::<_, MetastoreError>(acc) + } + None => Ok(acc), + } + }) + } + + fn request_value_to_data(&self, v: &OAuthRequestValue) -> Result { + let parameters = serde_json::from_str(&v.parameters_json) + .map_err(|_| MetastoreError::CorruptData("corrupt oauth request parameters"))?; + let client_auth = v + .client_auth_json + .as_ref() + .map(|j| serde_json::from_str(j)) + .transpose() + .map_err(|_| MetastoreError::CorruptData("corrupt oauth client_auth"))?; + + Ok(RequestData { + client_id: v.client_id.clone(), + client_auth, + parameters, + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), + did: v + .did + .as_ref() + .map(|d| Did::new(d.clone())) + .transpose() + .map_err(|_| MetastoreError::CorruptData("invalid did in oauth request"))?, + device_id: v + .device_id + .as_ref() + .map(|d| tranquil_oauth::DeviceId(d.clone())), + code: v.code.as_ref().map(|c| tranquil_oauth::Code(c.clone())), + controller_did: v + .controller_did + .as_ref() + .map(|d| Did::new(d.clone())) + .transpose() + .map_err(|_| { + MetastoreError::CorruptData("invalid controller_did in oauth request") + })?, + }) + } + + fn data_to_request_value(&self, data: &RequestData) -> OAuthRequestValue { + OAuthRequestValue { + client_id: data.client_id.clone(), + client_auth_json: data + .client_auth + .as_ref() + .map(|ca| serde_json::to_string(ca).unwrap_or_default()), + parameters_json: serde_json::to_string(&data.parameters).unwrap_or_default(), + expires_at_ms: data.expires_at.timestamp_millis(), + did: data.did.as_ref().map(|d| d.to_string()), + device_id: data.device_id.as_ref().map(|d| d.0.clone()), + code: data.code.as_ref().map(|c| c.0.clone()), + controller_did: data.controller_did.as_ref().map(|d| d.to_string()), + } + } + + pub fn create_token(&self, data: &TokenData) -> Result { + let user_hash = self.resolve_user_hash_from_did(data.did.as_str()); + let family_id = self.next_family_id()?; + let now_ms = Utc::now().timestamp_millis(); + + let value = OAuthTokenValue { + family_id, + did: data.did.to_string(), + client_id: data.client_id.clone(), + token_id: data.token_id.0.clone(), + refresh_token: data + .current_refresh_token + .as_ref() + .map(|r| r.0.clone()) + .unwrap_or_default(), + previous_refresh_token: None, + scope: data.scope.clone().unwrap_or_default(), + expires_at_ms: data.expires_at.timestamp_millis(), + created_at_ms: data.created_at.timestamp_millis(), + updated_at_ms: now_ms, + parameters_json: serde_json::to_string(&data.parameters).unwrap_or_default(), + controller_did: data.controller_did.as_ref().map(|d| d.to_string()), + }; + + let index = TokenIndexValue { + user_hash: user_hash.raw(), + family_id, + }; + + let mut batch = self.db.batch(); + batch.insert( + &self.auth, + oauth_token_key(user_hash, family_id).as_slice(), + value.serialize_with_ttl(), + ); + batch.insert( + &self.auth, + oauth_token_by_id_key(&value.token_id).as_slice(), + index.serialize_with_ttl(value.expires_at_ms), + ); + if !value.refresh_token.is_empty() { + batch.insert( + &self.auth, + oauth_token_by_refresh_key(&value.refresh_token).as_slice(), + index.serialize_with_ttl(value.expires_at_ms), + ); + } + batch.insert( + &self.auth, + oauth_token_by_family_key(family_id).as_slice(), + index.serialize_with_ttl(value.expires_at_ms), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(TokenFamilyId::new(family_id)) + } + + pub fn get_token_by_id(&self, token_id: &TokenId) -> Result, MetastoreError> { + let index_key = oauth_token_by_id_key(token_id.as_str()); + let index: Option = point_lookup( + &self.auth, + index_key.as_slice(), + TokenIndexValue::deserialize, + "corrupt oauth token index", + )?; + + match index { + Some(idx) => { + let uh = UserHash::from_raw(idx.user_hash); + self.load_token_by_family_id(uh, idx.family_id)? + .map(|v| self.token_value_to_data(&v)) + .transpose() + } + None => Ok(None), + } + } + + pub fn get_token_by_refresh_token( + &self, + refresh_token: &RefreshToken, + ) -> Result, MetastoreError> { + let index_key = oauth_token_by_refresh_key(refresh_token.as_str()); + let index: Option = point_lookup( + &self.auth, + index_key.as_slice(), + TokenIndexValue::deserialize, + "corrupt oauth token refresh index", + )?; + + match index { + Some(idx) => { + let uh = UserHash::from_raw(idx.user_hash); + self.load_token_by_family_id(uh, idx.family_id)? + .map(|v| { + self.token_value_to_data(&v) + .map(|td| (TokenFamilyId::new(idx.family_id), td)) + }) + .transpose() + } + None => Ok(None), + } + } + + pub fn get_token_by_previous_refresh_token( + &self, + refresh_token: &RefreshToken, + ) -> Result, MetastoreError> { + let index_key = oauth_token_by_prev_refresh_key(refresh_token.as_str()); + let index: Option = point_lookup( + &self.auth, + index_key.as_slice(), + TokenIndexValue::deserialize, + "corrupt oauth token prev refresh index", + )?; + + match index { + Some(idx) => { + let uh = UserHash::from_raw(idx.user_hash); + self.load_token_by_family_id(uh, idx.family_id)? + .map(|v| { + self.token_value_to_data(&v) + .map(|td| (TokenFamilyId::new(idx.family_id), td)) + }) + .transpose() + } + None => Ok(None), + } + } + + fn lookup_by_family_id( + &self, + family_id: i32, + ) -> Result, MetastoreError> { + let index_key = oauth_token_by_family_key(family_id); + let index: Option = point_lookup( + &self.auth, + index_key.as_slice(), + TokenIndexValue::deserialize, + "corrupt oauth token family index", + )?; + match index { + Some(idx) => { + let uh = UserHash::from_raw(idx.user_hash); + Ok(self + .load_token_by_family_id(uh, idx.family_id)? + .map(|v| (uh, v))) + } + None => Ok(None), + } + } + + pub fn rotate_token( + &self, + old_db_id: TokenFamilyId, + new_refresh_token: &RefreshToken, + new_expires_at: DateTime, + ) -> Result<(), MetastoreError> { + let (user_hash, mut token) = self + .lookup_by_family_id(old_db_id.as_i32())? + .ok_or(MetastoreError::InvalidInput("token family not found"))?; + + let old_refresh = token.refresh_token.clone(); + let old_prev = token.previous_refresh_token.clone(); + + token.previous_refresh_token = Some(old_refresh.clone()); + token.refresh_token = new_refresh_token.as_str().to_owned(); + token.expires_at_ms = new_expires_at.timestamp_millis(); + token.updated_at_ms = Utc::now().timestamp_millis(); + + let index = TokenIndexValue { + user_hash: user_hash.raw(), + family_id: token.family_id, + }; + + let mut batch = self.db.batch(); + batch.remove( + &self.auth, + oauth_token_by_refresh_key(&old_refresh).as_slice(), + ); + if let Some(prev) = &old_prev { + batch.remove(&self.auth, oauth_token_by_prev_refresh_key(prev).as_slice()); + } + + batch.insert( + &self.auth, + oauth_token_key(user_hash, token.family_id).as_slice(), + token.serialize_with_ttl(), + ); + batch.insert( + &self.auth, + oauth_token_by_refresh_key(new_refresh_token.as_str()).as_slice(), + index.serialize_with_ttl(token.expires_at_ms), + ); + batch.insert( + &self.auth, + oauth_token_by_prev_refresh_key(&old_refresh).as_slice(), + index.serialize_with_ttl(token.expires_at_ms), + ); + batch.insert( + &self.auth, + oauth_token_by_id_key(&token.token_id).as_slice(), + index.serialize_with_ttl(token.expires_at_ms), + ); + batch.insert( + &self.auth, + oauth_token_by_family_key(token.family_id).as_slice(), + index.serialize_with_ttl(token.expires_at_ms), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(()) + } + + pub fn check_refresh_token_used( + &self, + refresh_token: &RefreshToken, + ) -> Result, MetastoreError> { + let key = oauth_used_refresh_key(refresh_token.as_str()); + match self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => { + Ok(UsedRefreshValue::deserialize(&raw).map(|v| TokenFamilyId::new(v.family_id))) + } + None => Ok(None), + } + } + + pub fn delete_token(&self, token_id: &TokenId) -> Result<(), MetastoreError> { + let index_key = oauth_token_by_id_key(token_id.as_str()); + let index: Option = point_lookup( + &self.auth, + index_key.as_slice(), + TokenIndexValue::deserialize, + "corrupt oauth token index", + )?; + + let idx = match index { + Some(idx) => idx, + None => return Ok(()), + }; + + let uh = UserHash::from_raw(idx.user_hash); + let token = match self.load_token_by_family_id(uh, idx.family_id)? { + Some(t) => t, + None => return Ok(()), + }; + + let mut batch = self.db.batch(); + self.delete_token_indexes(&mut batch, &token, uh); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(()) + } + + pub fn delete_token_family(&self, db_id: TokenFamilyId) -> Result<(), MetastoreError> { + let (user_hash, token) = match self.lookup_by_family_id(db_id.as_i32())? { + Some(f) => f, + None => return Ok(()), + }; + + let used_val = UsedRefreshValue { + family_id: token.family_id, + }; + + let mut batch = self.db.batch(); + self.delete_token_indexes(&mut batch, &token, user_hash); + if !token.refresh_token.is_empty() { + batch.insert( + &self.auth, + oauth_used_refresh_key(&token.refresh_token).as_slice(), + used_val.serialize_with_ttl(token.expires_at_ms), + ); + } + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(()) + } + + pub fn list_tokens_for_user(&self, did: &Did) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let tokens = self.collect_tokens_for_did(user_hash)?; + tokens.iter().map(|v| self.token_value_to_data(v)).collect() + } + + pub fn count_tokens_for_user(&self, did: &Did) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let prefix = oauth_token_user_prefix(user_hash); + super::scan::count_prefix(&self.auth, prefix.as_slice()) + } + + pub fn delete_oldest_tokens_for_user( + &self, + did: &Did, + keep_count: i64, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let mut tokens = self.collect_tokens_for_did(user_hash)?; + tokens.sort_by_key(|t| std::cmp::Reverse(t.created_at_ms)); + + let keep = usize::try_from(keep_count).unwrap_or(usize::MAX); + let to_delete: Vec<_> = tokens.into_iter().skip(keep).collect(); + let count = u64::try_from(to_delete.len()).unwrap_or(u64::MAX); + + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + to_delete.iter().for_each(|token| { + self.delete_token_indexes(&mut batch, token, user_hash); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn revoke_tokens_for_client( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let tokens = self.collect_tokens_for_did(user_hash)?; + let to_revoke: Vec<_> = tokens + .into_iter() + .filter(|t| t.client_id == client_id.as_str()) + .collect(); + + let count = u64::try_from(to_revoke.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + to_revoke.iter().for_each(|token| { + self.delete_token_indexes(&mut batch, token, user_hash); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn revoke_tokens_for_controller( + &self, + delegated_did: &Did, + controller_did: &Did, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(delegated_did.as_str()); + let tokens = self.collect_tokens_for_did(user_hash)?; + let controller_str = controller_did.to_string(); + let to_revoke: Vec<_> = tokens + .into_iter() + .filter(|t| t.controller_did.as_deref() == Some(controller_str.as_str())) + .collect(); + + let count = u64::try_from(to_revoke.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + to_revoke.iter().for_each(|token| { + self.delete_token_indexes(&mut batch, token, user_hash); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn create_authorization_request( + &self, + request_id: &RequestId, + data: &RequestData, + ) -> Result<(), MetastoreError> { + let value = self.data_to_request_value(data); + let key = oauth_auth_request_key(request_id.as_str()); + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_authorization_request( + &self, + request_id: &RequestId, + ) -> Result, MetastoreError> { + let key = oauth_auth_request_key(request_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )?; + + val.map(|v| self.request_value_to_data(&v)).transpose() + } + + pub fn set_authorization_did( + &self, + request_id: &RequestId, + did: &Did, + device_id: Option<&DeviceId>, + ) -> Result<(), MetastoreError> { + let key = oauth_auth_request_key(request_id.as_str()); + let mut value: OAuthRequestValue = point_lookup( + &self.auth, + key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )? + .ok_or(MetastoreError::InvalidInput("auth request not found"))?; + + value.did = Some(did.to_string()); + value.device_id = device_id.map(|d| d.as_str().to_owned()); + + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall) + } + + pub fn update_authorization_request( + &self, + request_id: &RequestId, + did: &Did, + device_id: Option<&DeviceId>, + code: &AuthorizationCode, + ) -> Result<(), MetastoreError> { + let key = oauth_auth_request_key(request_id.as_str()); + let mut value: OAuthRequestValue = point_lookup( + &self.auth, + key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )? + .ok_or(MetastoreError::InvalidInput("auth request not found"))?; + + value.did = Some(did.to_string()); + value.device_id = device_id.map(|d| d.as_str().to_owned()); + value.code = Some(code.as_str().to_owned()); + + let code_index_key = oauth_auth_by_code_key(code.as_str()); + + let mut batch = self.db.batch(); + batch.insert(&self.auth, key.as_slice(), value.serialize_with_ttl()); + batch.insert( + &self.auth, + code_index_key.as_slice(), + request_id.as_str().as_bytes(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn consume_authorization_request_by_code( + &self, + code: &AuthorizationCode, + ) -> Result, MetastoreError> { + let code_key = oauth_auth_by_code_key(code.as_str()); + let request_id_bytes = match self + .auth + .get(code_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(bytes) => bytes, + None => return Ok(None), + }; + + let request_id_str = std::str::from_utf8(&request_id_bytes) + .map_err(|_| MetastoreError::CorruptData("corrupt code index value"))?; + let req_key = oauth_auth_request_key(request_id_str); + + let value: Option = point_lookup( + &self.auth, + req_key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )?; + + let data = match value { + Some(v) => self.request_value_to_data(&v)?, + None => return Ok(None), + }; + + let mut batch = self.db.batch(); + batch.remove(&self.auth, req_key.as_slice()); + batch.remove(&self.auth, code_key.as_slice()); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(Some(data)) + } + + pub fn delete_authorization_request( + &self, + request_id: &RequestId, + ) -> Result<(), MetastoreError> { + let key = oauth_auth_request_key(request_id.as_str()); + let value: Option = point_lookup( + &self.auth, + key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )?; + + let mut batch = self.db.batch(); + batch.remove(&self.auth, key.as_slice()); + if let Some(code) = value.and_then(|v| v.code) { + batch.remove(&self.auth, oauth_auth_by_code_key(&code).as_slice()); + } + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn delete_expired_authorization_requests(&self) -> Result { + let now_ms = Utc::now().timestamp_millis(); + let prefix = oauth_auth_request_prefix(); + let mut keys_to_remove: Vec<(Vec, Option)> = Vec::new(); + + self.auth.prefix(prefix.as_slice()).try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + match OAuthRequestValue::deserialize(&val_bytes) { + Some(v) if v.expires_at_ms <= now_ms => { + keys_to_remove.push((key_bytes.to_vec(), v.code)); + Ok::<(), MetastoreError>(()) + } + _ => Ok(()), + } + })?; + + let count = u64::try_from(keys_to_remove.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + keys_to_remove.iter().for_each(|(key, code)| { + batch.remove(&self.auth, key); + if let Some(c) = code { + batch.remove(&self.auth, oauth_auth_by_code_key(c).as_slice()); + } + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn extend_authorization_request_expiry( + &self, + request_id: &RequestId, + new_expires_at: DateTime, + ) -> Result { + let key = oauth_auth_request_key(request_id.as_str()); + let value: Option = point_lookup( + &self.auth, + key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )?; + + match value { + Some(mut v) => { + v.expires_at_ms = new_expires_at.timestamp_millis(); + self.auth + .insert(key.as_slice(), v.serialize_with_ttl()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + None => Ok(false), + } + } + + pub fn mark_request_authenticated( + &self, + request_id: &RequestId, + did: &Did, + device_id: Option<&DeviceId>, + ) -> Result<(), MetastoreError> { + self.set_authorization_did(request_id, did, device_id) + } + + pub fn update_request_scope( + &self, + request_id: &RequestId, + scope: &str, + ) -> Result<(), MetastoreError> { + let key = oauth_auth_request_key(request_id.as_str()); + let mut value: OAuthRequestValue = point_lookup( + &self.auth, + key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )? + .ok_or(MetastoreError::InvalidInput("auth request not found"))?; + + let mut params: serde_json::Value = serde_json::from_str(&value.parameters_json) + .map_err(|_| MetastoreError::CorruptData("corrupt parameters json"))?; + params["scope"] = serde_json::Value::String(scope.to_owned()); + value.parameters_json = serde_json::to_string(¶ms) + .map_err(|_| MetastoreError::CorruptData("json serialize fail"))?; + + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall) + } + + pub fn set_controller_did( + &self, + request_id: &RequestId, + controller_did: &Did, + ) -> Result<(), MetastoreError> { + let key = oauth_auth_request_key(request_id.as_str()); + let mut value: OAuthRequestValue = point_lookup( + &self.auth, + key.as_slice(), + OAuthRequestValue::deserialize, + "corrupt oauth auth request", + )? + .ok_or(MetastoreError::InvalidInput("auth request not found"))?; + + value.controller_did = Some(controller_did.to_string()); + + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall) + } + + pub fn set_request_did(&self, request_id: &RequestId, did: &Did) -> Result<(), MetastoreError> { + self.set_authorization_did(request_id, did, None) + } + + pub fn create_device( + &self, + device_id: &DeviceId, + data: &DeviceData, + ) -> Result<(), MetastoreError> { + let now_ms = Utc::now().timestamp_millis(); + let value = OAuthDeviceValue { + session_id: data.session_id.0.clone(), + user_agent: data.user_agent.clone(), + ip_address: data.ip_address.clone(), + last_seen_at_ms: data.last_seen_at.timestamp_millis(), + created_at_ms: now_ms, + }; + + let key = oauth_device_key(device_id.as_str()); + self.auth + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_device(&self, device_id: &DeviceId) -> Result, MetastoreError> { + let key = oauth_device_key(device_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + OAuthDeviceValue::deserialize, + "corrupt oauth device", + )?; + + Ok(val.map(|v| DeviceData { + session_id: tranquil_oauth::SessionId(v.session_id), + user_agent: v.user_agent, + ip_address: v.ip_address, + last_seen_at: DateTime::from_timestamp_millis(v.last_seen_at_ms).unwrap_or_default(), + })) + } + + pub fn update_device_last_seen(&self, device_id: &DeviceId) -> Result<(), MetastoreError> { + let key = oauth_device_key(device_id.as_str()); + let mut value: OAuthDeviceValue = point_lookup( + &self.auth, + key.as_slice(), + OAuthDeviceValue::deserialize, + "corrupt oauth device", + )? + .ok_or(MetastoreError::InvalidInput("device not found"))?; + + value.last_seen_at_ms = Utc::now().timestamp_millis(); + + self.auth + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn delete_device(&self, device_id: &DeviceId) -> Result<(), MetastoreError> { + let key = oauth_device_key(device_id.as_str()); + self.auth + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall) + } + + pub fn upsert_account_device( + &self, + did: &Did, + device_id: &DeviceId, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_account_device_key(user_hash, device_id.as_str()); + let now_ms = Utc::now().timestamp_millis(); + + let value = AccountDeviceValue { + last_used_at_ms: now_ms, + }; + + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_device_accounts( + &self, + device_id: &DeviceId, + ) -> Result, MetastoreError> { + let tag_prefix = [super::keys::KeyTag::OAUTH_ACCOUNT_DEVICE.raw()]; + let device_id_str = device_id.as_str(); + + self.auth + .prefix(tag_prefix) + .try_fold(Vec::new(), |mut acc, guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = match AccountDeviceValue::deserialize(&val_bytes) { + Some(v) => v, + None => return Ok(acc), + }; + + let mut reader = super::encoding::KeyReader::new(&key_bytes[1..]); + let user_hash_raw = reader + .u64() + .ok_or(MetastoreError::CorruptData("corrupt account device key"))?; + let stored_device_id = reader + .string() + .ok_or(MetastoreError::CorruptData("corrupt account device key"))?; + + if stored_device_id != device_id_str { + return Ok(acc); + } + + let uh = UserHash::from_raw(user_hash_raw); + if let Some(user) = self.load_user_value(uh)? { + let did = Did::new(user.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid did in user"))?; + let handle = Handle::new(user.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid handle in user"))?; + acc.push(DeviceAccountRow { + did, + handle, + email: user.email, + last_used_at: DateTime::from_timestamp_millis(val.last_used_at_ms) + .unwrap_or_default(), + }); + } + Ok(acc) + }) + } + + pub fn verify_account_on_device( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_account_device_key(user_hash, device_id.as_str()); + Ok(self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + .is_some()) + } + + pub fn check_and_record_dpop_jti(&self, jti: &DPoPProofId) -> Result { + let key = oauth_dpop_jti_key(jti.as_str()); + let existing = self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)?; + + match existing { + Some(_) => Ok(false), + None => { + let value = DpopJtiValue { + recorded_at_ms: Utc::now().timestamp_millis(), + }; + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + } + } + + pub fn cleanup_expired_dpop_jtis(&self, max_age_secs: i64) -> Result { + let cutoff_ms = Utc::now() + .timestamp_millis() + .saturating_sub(max_age_secs.saturating_mul(1000)); + let prefix = oauth_dpop_jti_prefix(); + let mut keys_to_remove: Vec> = Vec::new(); + + self.auth.prefix(prefix.as_slice()).try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + match DpopJtiValue::ttl_ms(&val_bytes) { + Some(recorded_ms) if (recorded_ms as i64) < cutoff_ms => { + keys_to_remove.push(key_bytes.to_vec()); + } + _ => {} + } + Ok::<(), MetastoreError>(()) + })?; + + let count = u64::try_from(keys_to_remove.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + keys_to_remove.iter().for_each(|key| { + batch.remove(&self.auth, key); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn create_2fa_challenge( + &self, + did: &Did, + request_uri: &RequestId, + ) -> Result { + let id = Uuid::new_v4(); + let now = Utc::now(); + let expires_at = now + Duration::minutes(10); + let code: String = { + let entropy = Uuid::new_v4(); + let bytes = entropy.as_bytes(); + (0..6) + .map(|i| std::char::from_digit(u32::from(bytes[i] % 10), 10).unwrap_or('0')) + .collect() + }; + + let value = TwoFactorChallengeValue { + id: *id.as_bytes(), + did: did.to_string(), + request_uri: request_uri.as_str().to_owned(), + code: code.clone(), + attempts: 0, + created_at_ms: now.timestamp_millis(), + expires_at_ms: expires_at.timestamp_millis(), + }; + + let primary_key = oauth_2fa_challenge_key(id.as_bytes()); + let request_index_key = oauth_2fa_by_request_key(request_uri.as_str()); + + let mut batch = self.db.batch(); + batch.insert( + &self.auth, + primary_key.as_slice(), + value.serialize_with_ttl(), + ); + batch.insert(&self.auth, request_index_key.as_slice(), id.as_bytes()); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(TwoFactorChallenge { + id, + did: did.clone(), + request_uri: request_uri.as_str().to_owned(), + code, + attempts: 0, + created_at: now, + expires_at, + }) + } + + pub fn get_2fa_challenge( + &self, + request_uri: &RequestId, + ) -> Result, MetastoreError> { + let index_key = oauth_2fa_by_request_key(request_uri.as_str()); + let id_bytes = match self + .auth + .get(index_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(bytes) => bytes, + None => return Ok(None), + }; + + let id_array: [u8; 16] = id_bytes + .as_ref() + .try_into() + .map_err(|_| MetastoreError::CorruptData("corrupt 2fa index"))?; + + let primary_key = oauth_2fa_challenge_key(&id_array); + let val: Option = point_lookup( + &self.auth, + primary_key.as_slice(), + TwoFactorChallengeValue::deserialize, + "corrupt 2fa challenge", + )?; + + val.map(|v| { + Ok(TwoFactorChallenge { + id: Uuid::from_bytes(v.id), + did: Did::new(v.did) + .map_err(|_| MetastoreError::CorruptData("invalid did in 2fa challenge"))?, + request_uri: v.request_uri, + code: v.code, + attempts: v.attempts, + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), + }) + }) + .transpose() + } + + pub fn increment_2fa_attempts(&self, id: Uuid) -> Result { + let primary_key = oauth_2fa_challenge_key(id.as_bytes()); + let mut value: TwoFactorChallengeValue = point_lookup( + &self.auth, + primary_key.as_slice(), + TwoFactorChallengeValue::deserialize, + "corrupt 2fa challenge", + )? + .ok_or(MetastoreError::InvalidInput("2fa challenge not found"))?; + + value.attempts = value.attempts.saturating_add(1); + + self.auth + .insert(primary_key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall)?; + + Ok(value.attempts) + } + + pub fn delete_2fa_challenge(&self, id: Uuid) -> Result<(), MetastoreError> { + let primary_key = oauth_2fa_challenge_key(id.as_bytes()); + let value: Option = point_lookup( + &self.auth, + primary_key.as_slice(), + TwoFactorChallengeValue::deserialize, + "corrupt 2fa challenge", + )?; + + let mut batch = self.db.batch(); + batch.remove(&self.auth, primary_key.as_slice()); + if let Some(v) = value { + batch.remove( + &self.auth, + oauth_2fa_by_request_key(&v.request_uri).as_slice(), + ); + } + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn delete_2fa_challenge_by_request_uri( + &self, + request_uri: &RequestId, + ) -> Result<(), MetastoreError> { + let index_key = oauth_2fa_by_request_key(request_uri.as_str()); + let id_bytes = match self + .auth + .get(index_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(bytes) => bytes, + None => return Ok(()), + }; + + let id_array: [u8; 16] = match id_bytes.as_ref().try_into() { + Ok(arr) => arr, + Err(_) => return Ok(()), + }; + + let primary_key = oauth_2fa_challenge_key(&id_array); + let mut batch = self.db.batch(); + batch.remove(&self.auth, primary_key.as_slice()); + batch.remove(&self.auth, index_key.as_slice()); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn cleanup_expired_2fa_challenges(&self) -> Result { + let now_ms = Utc::now().timestamp_millis(); + let prefix = oauth_2fa_challenge_prefix(); + let mut to_remove: Vec<(Vec, String)> = Vec::new(); + + self.auth.prefix(prefix.as_slice()).try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + match TwoFactorChallengeValue::ttl_ms(&val_bytes) { + Some(expires_ms) if (expires_ms as i64) <= now_ms => { + let v = TwoFactorChallengeValue::deserialize(&val_bytes); + let request_uri = v.map(|v| v.request_uri).unwrap_or_default(); + to_remove.push((key_bytes.to_vec(), request_uri)); + } + _ => {} + } + Ok::<(), MetastoreError>(()) + })?; + + let count = u64::try_from(to_remove.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + to_remove.iter().for_each(|(key, request_uri)| { + batch.remove(&self.auth, key); + if !request_uri.is_empty() { + batch.remove(&self.auth, oauth_2fa_by_request_key(request_uri).as_slice()); + } + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn check_user_2fa_enabled(&self, did: &Did) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let user = self.load_user_value(user_hash)?; + Ok(user + .map(|u| u.two_factor_enabled || u.totp_enabled || u.email_2fa_enabled) + .unwrap_or(false)) + } + + pub fn get_scope_preferences( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_scope_prefs_key(user_hash, client_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + ScopePrefsValue::deserialize, + "corrupt scope preferences", + )?; + + match val { + Some(v) => serde_json::from_str(&v.prefs_json) + .map_err(|_| MetastoreError::CorruptData("corrupt scope prefs json")), + None => Ok(Vec::new()), + } + } + + pub fn upsert_scope_preferences( + &self, + did: &Did, + client_id: &ClientId, + prefs: &[ScopePreference], + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_scope_prefs_key(user_hash, client_id.as_str()); + let prefs_json = serde_json::to_string(prefs) + .map_err(|_| MetastoreError::CorruptData("scope prefs serialize fail"))?; + + let value = ScopePrefsValue { prefs_json }; + self.auth + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn delete_scope_preferences( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_scope_prefs_key(user_hash, client_id.as_str()); + self.auth + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall) + } + + pub fn upsert_authorized_client( + &self, + did: &Did, + client_id: &ClientId, + data: &AuthorizedClientData, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_auth_client_key(user_hash, client_id.as_str()); + let data_json = serde_json::to_string(data) + .map_err(|_| MetastoreError::CorruptData("authorized client serialize fail"))?; + + let value = AuthorizedClientValue { data_json }; + self.auth + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_authorized_client( + &self, + did: &Did, + client_id: &ClientId, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_auth_client_key(user_hash, client_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + AuthorizedClientValue::deserialize, + "corrupt authorized client", + )?; + + match val { + Some(v) => serde_json::from_str(&v.data_json) + .map_err(|_| MetastoreError::CorruptData("corrupt authorized client json")) + .map(Some), + None => Ok(None), + } + } + + pub fn list_trusted_devices(&self, did: &Did) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let prefix = oauth_device_trust_prefix(user_hash); + + self.auth + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + match DeviceTrustValue::deserialize(&val_bytes) { + Some(v) => { + acc.push(TrustedDeviceRow { + id: v.device_id, + user_agent: v.user_agent, + friendly_name: v.friendly_name, + trusted_at: v.trusted_at_ms.and_then(DateTime::from_timestamp_millis), + trusted_until: v + .trusted_until_ms + .and_then(DateTime::from_timestamp_millis), + last_seen_at: DateTime::from_timestamp_millis(v.last_seen_at_ms) + .unwrap_or_default(), + }); + Ok(acc) + } + None => Ok(acc), + } + }) + } + + pub fn get_device_trust_info( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_device_trust_key(user_hash, device_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + DeviceTrustValue::deserialize, + "corrupt device trust", + )?; + + Ok(val.map(|v| DeviceTrustInfo { + trusted_at: v.trusted_at_ms.and_then(DateTime::from_timestamp_millis), + trusted_until: v.trusted_until_ms.and_then(DateTime::from_timestamp_millis), + })) + } + + pub fn device_belongs_to_user( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_account_device_key(user_hash, device_id.as_str()); + Ok(self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + .is_some()) + } + + pub fn revoke_device_trust( + &self, + device_id: &DeviceId, + did: &Did, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_device_trust_key(user_hash, device_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + DeviceTrustValue::deserialize, + "corrupt device trust", + )?; + match val { + Some(mut v) => { + v.trusted_at_ms = None; + v.trusted_until_ms = None; + self.auth + .insert(key.as_slice(), v.serialize()) + .map_err(MetastoreError::Fjall) + } + None => Ok(()), + } + } + + pub fn update_device_friendly_name( + &self, + device_id: &DeviceId, + did: &Did, + friendly_name: Option<&str>, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_device_trust_key(user_hash, device_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + DeviceTrustValue::deserialize, + "corrupt device trust", + )?; + match val { + Some(mut v) => { + v.friendly_name = friendly_name.map(str::to_owned); + self.auth + .insert(key.as_slice(), v.serialize()) + .map_err(MetastoreError::Fjall) + } + None => Ok(()), + } + } + + pub fn trust_device( + &self, + device_id: &DeviceId, + did: &Did, + trusted_at: DateTime, + trusted_until: DateTime, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_device_trust_key(user_hash, device_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + DeviceTrustValue::deserialize, + "corrupt device trust", + )?; + match val { + Some(mut v) => { + v.trusted_at_ms = Some(trusted_at.timestamp_millis()); + v.trusted_until_ms = Some(trusted_until.timestamp_millis()); + self.auth + .insert(key.as_slice(), v.serialize()) + .map_err(MetastoreError::Fjall) + } + None => Ok(()), + } + } + + pub fn extend_device_trust( + &self, + device_id: &DeviceId, + did: &Did, + trusted_until: DateTime, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = oauth_device_trust_key(user_hash, device_id.as_str()); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + DeviceTrustValue::deserialize, + "corrupt device trust", + )?; + match val { + Some(mut v) => { + v.trusted_until_ms = Some(trusted_until.timestamp_millis()); + self.auth + .insert(key.as_slice(), v.serialize()) + .map_err(MetastoreError::Fjall) + } + None => Ok(()), + } + } + + pub fn list_sessions_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let now_ms = Utc::now().timestamp_millis(); + let tokens = self.collect_tokens_for_did(user_hash)?; + + Ok(tokens + .iter() + .filter(|t| t.expires_at_ms > now_ms) + .map(|t| OAuthSessionListItem { + id: TokenFamilyId::new(t.family_id), + token_id: TokenId::new(t.token_id.clone()), + created_at: DateTime::from_timestamp_millis(t.created_at_ms).unwrap_or_default(), + expires_at: DateTime::from_timestamp_millis(t.expires_at_ms).unwrap_or_default(), + client_id: ClientId::new(t.client_id.clone()), + }) + .collect()) + } + + pub fn delete_session_by_id( + &self, + session_id: TokenFamilyId, + did: &Did, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let token = match self.load_token_by_family_id(user_hash, session_id.as_i32())? { + Some(t) => t, + None => return Ok(0), + }; + + let mut batch = self.db.batch(); + self.delete_token_indexes(&mut batch, &token, user_hash); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(1) + } + + pub fn delete_sessions_by_did(&self, did: &Did) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let tokens = self.collect_tokens_for_did(user_hash)?; + + let count = u64::try_from(tokens.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + tokens.iter().for_each(|token| { + self.delete_token_indexes(&mut batch, token, user_hash); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn delete_sessions_by_did_except( + &self, + did: &Did, + except_token_id: &TokenId, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let tokens = self.collect_tokens_for_did(user_hash)?; + let except_str = except_token_id.as_str(); + + let to_delete: Vec<_> = tokens.iter().filter(|t| t.token_id != except_str).collect(); + + let count = u64::try_from(to_delete.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + to_delete.iter().for_each(|token| { + self.delete_token_indexes(&mut batch, token, user_hash); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } +} + +fn default_parameters(client_id: &str) -> tranquil_oauth::AuthorizationRequestParameters { + tranquil_oauth::AuthorizationRequestParameters { + response_type: tranquil_oauth::ResponseType::Code, + client_id: client_id.to_owned(), + redirect_uri: String::new(), + scope: None, + state: None, + code_challenge: String::new(), + code_challenge_method: tranquil_oauth::CodeChallengeMethod::S256, + response_mode: None, + login_hint: None, + dpop_jkt: None, + prompt: None, + extra: None, + } +} diff --git a/crates/tranquil-store/src/metastore/oauth_schema.rs b/crates/tranquil-store/src/metastore/oauth_schema.rs new file mode 100644 index 0000000..07dfdcd --- /dev/null +++ b/crates/tranquil-store/src/metastore/oauth_schema.rs @@ -0,0 +1,556 @@ +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +use super::encoding::KeyBuilder; +use super::keys::{KeyTag, UserHash}; + +const TOKEN_SCHEMA_VERSION: u8 = 1; +const REQUEST_SCHEMA_VERSION: u8 = 1; +const DEVICE_SCHEMA_VERSION: u8 = 1; +const ACCOUNT_DEVICE_SCHEMA_VERSION: u8 = 1; +const DPOP_JTI_SCHEMA_VERSION: u8 = 1; +const CHALLENGE_SCHEMA_VERSION: u8 = 1; +const SCOPE_PREFS_SCHEMA_VERSION: u8 = 1; +const AUTH_CLIENT_SCHEMA_VERSION: u8 = 1; +const DEVICE_TRUST_SCHEMA_VERSION: u8 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OAuthTokenValue { + pub family_id: i32, + pub did: String, + pub client_id: String, + pub token_id: String, + pub refresh_token: String, + pub previous_refresh_token: Option, + pub scope: String, + pub expires_at_ms: i64, + pub created_at_ms: i64, + pub updated_at_ms: i64, + pub parameters_json: String, + pub controller_did: Option, +} + +impl OAuthTokenValue { + pub fn serialize_with_ttl(&self) -> Vec { + let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("OAuthTokenValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(TOKEN_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + if bytes.len() < 9 { + return None; + } + let (_ttl, rest) = bytes.split_at(8); + let (&version, payload) = rest.split_first()?; + match version { + TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OAuthRequestValue { + pub client_id: String, + pub client_auth_json: Option, + pub parameters_json: String, + pub expires_at_ms: i64, + pub did: Option, + pub device_id: Option, + pub code: Option, + pub controller_did: Option, +} + +impl OAuthRequestValue { + pub fn serialize_with_ttl(&self) -> Vec { + let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("OAuthRequestValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(REQUEST_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + if bytes.len() < 9 { + return None; + } + let (_ttl, rest) = bytes.split_at(8); + let (&version, payload) = rest.split_first()?; + match version { + REQUEST_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OAuthDeviceValue { + pub session_id: String, + pub user_agent: Option, + pub ip_address: String, + pub last_seen_at_ms: i64, + pub created_at_ms: i64, +} + +impl OAuthDeviceValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("OAuthDeviceValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.push(DEVICE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + DEVICE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AccountDeviceValue { + pub last_used_at_ms: i64, +} + +impl AccountDeviceValue { + pub fn serialize_with_ttl(&self) -> Vec { + let ttl_bytes = 0u64.to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("AccountDeviceValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(ACCOUNT_DEVICE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + if bytes.len() < 9 { + return None; + } + let (_ttl, rest) = bytes.split_at(8); + let (&version, payload) = rest.split_first()?; + match version { + ACCOUNT_DEVICE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DpopJtiValue { + pub recorded_at_ms: i64, +} + +const DPOP_JTI_WINDOW_MS: i64 = 300_000; + +impl DpopJtiValue { + pub fn serialize_with_ttl(&self) -> Vec { + let expires_at_ms = self.recorded_at_ms.saturating_add(DPOP_JTI_WINDOW_MS); + let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = postcard::to_allocvec(self).expect("DpopJtiValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(DPOP_JTI_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + if bytes.len() < 9 { + return None; + } + let (_ttl, rest) = bytes.split_at(8); + let (&version, payload) = rest.split_first()?; + match version { + DPOP_JTI_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } + + pub fn ttl_ms(bytes: &[u8]) -> Option { + if bytes.len() < 8 { + return None; + } + let ttl_bytes: [u8; 8] = bytes[..8].try_into().ok()?; + Some(u64::from_be_bytes(ttl_bytes)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TwoFactorChallengeValue { + pub id: [u8; 16], + pub did: String, + pub request_uri: String, + pub code: String, + pub attempts: i32, + pub created_at_ms: i64, + pub expires_at_ms: i64, +} + +impl TwoFactorChallengeValue { + pub fn serialize_with_ttl(&self) -> Vec { + let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("TwoFactorChallengeValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(CHALLENGE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + if bytes.len() < 9 { + return None; + } + let (_ttl, rest) = bytes.split_at(8); + let (&version, payload) = rest.split_first()?; + match version { + CHALLENGE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } + + pub fn ttl_ms(bytes: &[u8]) -> Option { + if bytes.len() < 8 { + return None; + } + let ttl_bytes: [u8; 8] = bytes[..8].try_into().ok()?; + Some(u64::from_be_bytes(ttl_bytes)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ScopePrefsValue { + pub prefs_json: String, +} + +impl ScopePrefsValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("ScopePrefsValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.push(SCOPE_PREFS_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + SCOPE_PREFS_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthorizedClientValue { + pub data_json: String, +} + +impl AuthorizedClientValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("AuthorizedClientValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.push(AUTH_CLIENT_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + AUTH_CLIENT_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DeviceTrustValue { + pub device_id: String, + pub did: String, + pub user_agent: Option, + pub friendly_name: Option, + pub trusted_at_ms: Option, + pub trusted_until_ms: Option, + pub last_seen_at_ms: i64, +} + +impl DeviceTrustValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("DeviceTrustValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.push(DEVICE_TRUST_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + DEVICE_TRUST_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UsedRefreshValue { + pub family_id: i32, +} + +impl UsedRefreshValue { + pub fn serialize_with_ttl(&self, expires_at_ms: i64) -> Vec { + let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("UsedRefreshValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(TOKEN_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + if bytes.len() < 9 { + return None; + } + let (_ttl, rest) = bytes.split_at(8); + let (&version, payload) = rest.split_first()?; + match version { + TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +pub fn oauth_token_key(user_hash: UserHash, family_id: i32) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_TOKEN) + .u64(user_hash.raw()) + .raw(&family_id.to_be_bytes()) + .build() +} + +pub fn oauth_token_user_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_TOKEN) + .u64(user_hash.raw()) + .build() +} + +pub fn oauth_token_by_id_key(token_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_TOKEN_BY_ID) + .string(token_id) + .build() +} + +pub fn oauth_token_by_refresh_key(refresh_token: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_TOKEN_BY_REFRESH) + .string(refresh_token) + .build() +} + +pub fn oauth_token_by_prev_refresh_key(refresh_token: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_TOKEN_BY_PREV_REFRESH) + .string(refresh_token) + .build() +} + +pub fn oauth_used_refresh_key(refresh_token: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_USED_REFRESH) + .string(refresh_token) + .build() +} + +pub fn oauth_auth_request_key(request_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_AUTH_REQUEST) + .string(request_id) + .build() +} + +pub fn oauth_auth_request_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::OAUTH_AUTH_REQUEST).build() +} + +pub fn oauth_auth_by_code_key(code: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_AUTH_BY_CODE) + .string(code) + .build() +} + +pub fn oauth_device_key(device_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_DEVICE) + .string(device_id) + .build() +} + +pub fn oauth_account_device_key(user_hash: UserHash, device_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_ACCOUNT_DEVICE) + .u64(user_hash.raw()) + .string(device_id) + .build() +} + +pub fn oauth_account_device_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_ACCOUNT_DEVICE) + .u64(user_hash.raw()) + .build() +} + +pub fn oauth_dpop_jti_key(jti: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_DPOP_JTI) + .string(jti) + .build() +} + +pub fn oauth_dpop_jti_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::OAUTH_DPOP_JTI).build() +} + +pub fn oauth_2fa_challenge_key(id: &[u8; 16]) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_2FA_CHALLENGE) + .raw(id) + .build() +} + +pub fn oauth_2fa_challenge_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::OAUTH_2FA_CHALLENGE).build() +} + +pub fn oauth_2fa_by_request_key(request_uri: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_2FA_BY_REQUEST) + .string(request_uri) + .build() +} + +pub fn oauth_scope_prefs_key(user_hash: UserHash, client_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_SCOPE_PREFS) + .u64(user_hash.raw()) + .string(client_id) + .build() +} + +pub fn oauth_auth_client_key(user_hash: UserHash, client_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_AUTH_CLIENT) + .u64(user_hash.raw()) + .string(client_id) + .build() +} + +pub fn oauth_device_trust_key(user_hash: UserHash, device_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_DEVICE_TRUST) + .u64(user_hash.raw()) + .string(device_id) + .build() +} + +pub fn oauth_device_trust_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_DEVICE_TRUST) + .u64(user_hash.raw()) + .build() +} + +pub fn oauth_token_family_counter_key() -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_TOKEN_FAMILY_COUNTER) + .build() +} + +pub fn serialize_family_counter(counter: i32) -> Vec { + let mut buf = Vec::with_capacity(12); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.extend_from_slice(&counter.to_be_bytes()); + buf +} + +pub fn deserialize_family_counter(bytes: &[u8]) -> Option { + match bytes.len() { + 4 => Some(i32::from_be_bytes(bytes.try_into().ok()?)), + n if n >= 12 => { + let arr: [u8; 4] = bytes[8..12].try_into().ok()?; + Some(i32::from_be_bytes(arr)) + } + _ => None, + } +} + +pub fn oauth_token_by_family_key(family_id: i32) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::OAUTH_TOKEN_BY_FAMILY) + .raw(&family_id.to_be_bytes()) + .build() +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TokenIndexValue { + pub user_hash: u64, + pub family_id: i32, +} + +impl TokenIndexValue { + pub fn serialize_with_ttl(&self, expires_at_ms: i64) -> Vec { + let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("TokenIndexValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(TOKEN_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + if bytes.len() < 9 { + return None; + } + let (_ttl, rest) = bytes.split_at(8); + let (&version, payload) = rest.split_first()?; + match version { + TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} diff --git a/crates/tranquil-store/src/metastore/session_ops.rs b/crates/tranquil-store/src/metastore/session_ops.rs new file mode 100644 index 0000000..1138e6e --- /dev/null +++ b/crates/tranquil-store/src/metastore/session_ops.rs @@ -0,0 +1,919 @@ +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use fjall::{Database, Keyspace}; +use uuid::Uuid; + +use super::MetastoreError; +use super::keys::UserHash; +use super::scan::point_lookup; +use super::sessions::{ + AppPasswordValue, SessionIndexValue, SessionTokenValue, deserialize_id_counter_value, + deserialize_last_reauth_value, deserialize_used_refresh_value, login_type_to_u8, + privilege_to_u8, serialize_by_did_value, serialize_id_counter_value, + serialize_last_reauth_value, serialize_used_refresh_value, session_app_password_key, + session_app_password_prefix, session_by_access_key, session_by_did_key, session_by_did_prefix, + session_by_refresh_key, session_id_counter_key, session_last_reauth_key, session_primary_key, + session_used_refresh_key, u8_to_login_type, u8_to_privilege, +}; +use super::user_hash::UserHashMap; +use super::users::UserValue; + +use tranquil_db_traits::{ + AppPasswordCreate, AppPasswordRecord, LoginType, RefreshSessionResult, SessionForRefresh, + SessionId, SessionListItem, SessionMfaStatus, SessionRefreshData, SessionToken, + SessionTokenCreate, +}; +use tranquil_types::Did; + +pub struct SessionOps { + db: Database, + auth: Keyspace, + users: Keyspace, + user_hashes: Arc, + counter_lock: Arc>, +} + +impl SessionOps { + pub fn new( + db: Database, + auth: Keyspace, + users: Keyspace, + user_hashes: Arc, + counter_lock: Arc>, + ) -> Self { + Self { + db, + auth, + users, + user_hashes, + counter_lock, + } + } + + fn resolve_user_hash_from_did(&self, did: &str) -> UserHash { + UserHash::from_did(did) + } + + fn resolve_user_hash_from_uuid(&self, user_id: Uuid) -> Result { + self.user_hashes + .get(&user_id) + .ok_or(MetastoreError::InvalidInput("unknown user_id")) + } + + fn next_session_id(&self) -> Result { + let _guard = self.counter_lock.lock(); + let counter_key = session_id_counter_key(); + let current = self + .auth + .get(counter_key.as_slice()) + .map_err(MetastoreError::Fjall)? + .and_then(|raw| deserialize_id_counter_value(&raw)) + .unwrap_or(0); + let next = current.saturating_add(1); + self.auth + .insert(counter_key.as_slice(), serialize_id_counter_value(next)) + .map_err(MetastoreError::Fjall)?; + Ok(next) + } + + fn value_to_session_token( + &self, + v: &SessionTokenValue, + ) -> Result { + Ok(SessionToken { + id: SessionId::new(v.id), + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid session did"))?, + access_jti: v.access_jti.clone(), + refresh_jti: v.refresh_jti.clone(), + access_expires_at: DateTime::from_timestamp_millis(v.access_expires_at_ms) + .unwrap_or_default(), + refresh_expires_at: DateTime::from_timestamp_millis(v.refresh_expires_at_ms) + .unwrap_or_default(), + login_type: u8_to_login_type(v.login_type).unwrap_or(LoginType::Modern), + mfa_verified: v.mfa_verified, + scope: v.scope.clone(), + controller_did: v + .controller_did + .as_ref() + .and_then(|d| Did::new(d.clone()).ok()), + app_password_name: v.app_password_name.clone(), + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + updated_at: DateTime::from_timestamp_millis(v.updated_at_ms).unwrap_or_default(), + }) + } + + fn value_to_app_password( + &self, + v: &AppPasswordValue, + ) -> Result { + Ok(AppPasswordRecord { + id: v.id, + user_id: v.user_id, + name: v.name.clone(), + password_hash: v.password_hash.clone(), + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + privilege: u8_to_privilege(v.privilege) + .unwrap_or(tranquil_db_traits::AppPasswordPrivilege::Standard), + scopes: v.scopes.clone(), + created_by_controller_did: v + .created_by_controller_did + .as_ref() + .and_then(|d| Did::new(d.clone()).ok()), + }) + } + + fn load_session_by_id( + &self, + session_id: i32, + ) -> Result, MetastoreError> { + let key = session_primary_key(session_id); + point_lookup( + &self.auth, + key.as_slice(), + SessionTokenValue::deserialize, + "corrupt session token", + ) + } + + fn load_user_value(&self, user_hash: UserHash) -> Result, MetastoreError> { + let key = super::encoding::KeyBuilder::new() + .tag(super::keys::KeyTag::USER_PRIMARY) + .u64(user_hash.raw()) + .build(); + point_lookup( + &self.users, + key.as_slice(), + UserValue::deserialize, + "corrupt user value", + ) + } + + fn delete_session_indexes( + &self, + batch: &mut fjall::OwnedWriteBatch, + session: &SessionTokenValue, + ) { + let user_hash = self.resolve_user_hash_from_did(&session.did); + batch.remove(&self.auth, session_primary_key(session.id).as_slice()); + batch.remove( + &self.auth, + session_by_access_key(&session.access_jti).as_slice(), + ); + batch.remove( + &self.auth, + session_by_refresh_key(&session.refresh_jti).as_slice(), + ); + batch.remove( + &self.auth, + session_by_did_key(user_hash, session.id).as_slice(), + ); + } + + fn collect_sessions_for_did( + &self, + user_hash: UserHash, + ) -> Result, MetastoreError> { + let prefix = session_by_did_prefix(user_hash); + self.auth + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (key_bytes, _) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let mut reader = super::encoding::KeyReader::new(&key_bytes); + let _tag = reader.tag(); + let _hash = reader.u64(); + let sid_bytes: [u8; 4] = reader + .remaining() + .try_into() + .map_err(|_| MetastoreError::CorruptData("session_by_did key truncated"))?; + let sid = i32::from_be_bytes(sid_bytes); + match self.load_session_by_id(sid)? { + Some(session) => { + acc.push(session); + Ok::<_, MetastoreError>(acc) + } + None => Ok(acc), + } + }) + } + + pub fn create_session(&self, data: &SessionTokenCreate) -> Result { + let user_hash = self.resolve_user_hash_from_did(data.did.as_str()); + let session_id = self.next_session_id()?; + let now_ms = Utc::now().timestamp_millis(); + + let value = SessionTokenValue { + id: session_id, + did: data.did.to_string(), + access_jti: data.access_jti.clone(), + refresh_jti: data.refresh_jti.clone(), + access_expires_at_ms: data.access_expires_at.timestamp_millis(), + refresh_expires_at_ms: data.refresh_expires_at.timestamp_millis(), + login_type: login_type_to_u8(data.login_type), + mfa_verified: data.mfa_verified, + scope: data.scope.clone(), + controller_did: data.controller_did.as_ref().map(|d| d.to_string()), + app_password_name: data.app_password_name.clone(), + created_at_ms: now_ms, + updated_at_ms: now_ms, + }; + + let access_index = SessionIndexValue { + user_hash: user_hash.raw(), + session_id, + }; + let refresh_index = SessionIndexValue { + user_hash: user_hash.raw(), + session_id, + }; + + let mut batch = self.db.batch(); + batch.insert( + &self.auth, + session_primary_key(session_id).as_slice(), + value.serialize(), + ); + batch.insert( + &self.auth, + session_by_access_key(&data.access_jti).as_slice(), + access_index.serialize(value.refresh_expires_at_ms), + ); + batch.insert( + &self.auth, + session_by_refresh_key(&data.refresh_jti).as_slice(), + refresh_index.serialize(value.refresh_expires_at_ms), + ); + batch.insert( + &self.auth, + session_by_did_key(user_hash, session_id).as_slice(), + serialize_by_did_value(value.refresh_expires_at_ms), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(SessionId::new(session_id)) + } + + pub fn get_session_by_access_jti( + &self, + access_jti: &str, + ) -> Result, MetastoreError> { + let index_key = session_by_access_key(access_jti); + let index_val: Option = point_lookup( + &self.auth, + index_key.as_slice(), + SessionIndexValue::deserialize, + "corrupt session access index", + )?; + + match index_val { + Some(idx) => { + let session = self.load_session_by_id(idx.session_id)?; + session.map(|v| self.value_to_session_token(&v)).transpose() + } + None => Ok(None), + } + } + + pub fn get_session_for_refresh( + &self, + refresh_jti: &str, + ) -> Result, MetastoreError> { + let index_key = session_by_refresh_key(refresh_jti); + let index_val: Option = point_lookup( + &self.auth, + index_key.as_slice(), + SessionIndexValue::deserialize, + "corrupt session refresh index", + )?; + + let idx = match index_val { + Some(idx) => idx, + None => return Ok(None), + }; + + let session = match self.load_session_by_id(idx.session_id)? { + Some(s) => s, + None => return Ok(None), + }; + + let now_ms = Utc::now().timestamp_millis(); + if session.refresh_expires_at_ms <= now_ms { + return Ok(None); + } + + let user_hash = self.resolve_user_hash_from_did(&session.did); + let user_value = self.load_user_value(user_hash)?; + + match user_value { + Some(user) => Ok(Some(SessionForRefresh { + id: SessionId::new(session.id), + did: Did::new(session.did) + .map_err(|_| MetastoreError::CorruptData("invalid session did"))?, + scope: session.scope, + controller_did: session.controller_did.and_then(|d| Did::new(d).ok()), + key_bytes: user.key_bytes, + encryption_version: user.encryption_version, + })), + None => Ok(None), + } + } + + pub fn update_session_tokens( + &self, + session_id: SessionId, + new_access_jti: &str, + new_refresh_jti: &str, + new_access_expires_at: DateTime, + new_refresh_expires_at: DateTime, + ) -> Result<(), MetastoreError> { + let mut session = self + .load_session_by_id(session_id.as_i32())? + .ok_or(MetastoreError::InvalidInput("session not found"))?; + + let user_hash = self.resolve_user_hash_from_did(&session.did); + let old_access_jti = session.access_jti.clone(); + let old_refresh_jti = session.refresh_jti.clone(); + + session.access_jti = new_access_jti.to_owned(); + session.refresh_jti = new_refresh_jti.to_owned(); + session.access_expires_at_ms = new_access_expires_at.timestamp_millis(); + session.refresh_expires_at_ms = new_refresh_expires_at.timestamp_millis(); + session.updated_at_ms = Utc::now().timestamp_millis(); + + let new_access_index = SessionIndexValue { + user_hash: user_hash.raw(), + session_id: session.id, + }; + let new_refresh_index = SessionIndexValue { + user_hash: user_hash.raw(), + session_id: session.id, + }; + + let mut batch = self.db.batch(); + batch.remove( + &self.auth, + session_by_access_key(&old_access_jti).as_slice(), + ); + batch.remove( + &self.auth, + session_by_refresh_key(&old_refresh_jti).as_slice(), + ); + batch.insert( + &self.auth, + session_primary_key(session.id).as_slice(), + session.serialize(), + ); + batch.insert( + &self.auth, + session_by_access_key(new_access_jti).as_slice(), + new_access_index.serialize(session.refresh_expires_at_ms), + ); + batch.insert( + &self.auth, + session_by_refresh_key(new_refresh_jti).as_slice(), + new_refresh_index.serialize(session.refresh_expires_at_ms), + ); + batch.insert( + &self.auth, + session_by_did_key(user_hash, session.id).as_slice(), + serialize_by_did_value(session.refresh_expires_at_ms), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(()) + } + + pub fn delete_session_by_access_jti(&self, access_jti: &str) -> Result { + let index_key = session_by_access_key(access_jti); + let index_val: Option = point_lookup( + &self.auth, + index_key.as_slice(), + SessionIndexValue::deserialize, + "corrupt session access index", + )?; + + let idx = match index_val { + Some(idx) => idx, + None => return Ok(0), + }; + + let session = match self.load_session_by_id(idx.session_id)? { + Some(s) => s, + None => return Ok(0), + }; + + let mut batch = self.db.batch(); + self.delete_session_indexes(&mut batch, &session); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(1) + } + + pub fn delete_session_by_id(&self, session_id: SessionId) -> Result { + let session = match self.load_session_by_id(session_id.as_i32())? { + Some(s) => s, + None => return Ok(0), + }; + + let mut batch = self.db.batch(); + self.delete_session_indexes(&mut batch, &session); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(1) + } + + pub fn delete_sessions_by_did(&self, did: &Did) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let sessions = self.collect_sessions_for_did(user_hash)?; + + let count = u64::try_from(sessions.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + sessions.iter().for_each(|session| { + self.delete_session_indexes(&mut batch, session); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn delete_sessions_by_did_except_jti( + &self, + did: &Did, + except_jti: &str, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let sessions = self.collect_sessions_for_did(user_hash)?; + + let to_delete: Vec<_> = sessions + .iter() + .filter(|s| s.access_jti != except_jti) + .collect(); + + let count = u64::try_from(to_delete.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + to_delete.iter().for_each(|session| { + self.delete_session_indexes(&mut batch, session); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn list_sessions_by_did(&self, did: &Did) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let now_ms = Utc::now().timestamp_millis(); + let mut sessions = self.collect_sessions_for_did(user_hash)?; + sessions.retain(|s| s.refresh_expires_at_ms > now_ms); + sessions.sort_by_key(|s| std::cmp::Reverse(s.created_at_ms)); + + Ok(sessions + .iter() + .map(|s| SessionListItem { + id: SessionId::new(s.id), + access_jti: s.access_jti.clone(), + created_at: DateTime::from_timestamp_millis(s.created_at_ms).unwrap_or_default(), + refresh_expires_at: DateTime::from_timestamp_millis(s.refresh_expires_at_ms) + .unwrap_or_default(), + }) + .collect()) + } + + pub fn get_session_access_jti_by_id( + &self, + session_id: SessionId, + did: &Did, + ) -> Result, MetastoreError> { + self.load_session_by_id(session_id.as_i32())? + .filter(|s| s.did == did.as_str()) + .map(|s| Ok(s.access_jti)) + .transpose() + } + + pub fn delete_sessions_by_app_password( + &self, + did: &Did, + app_password_name: &str, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let sessions = self.collect_sessions_for_did(user_hash)?; + + let to_delete: Vec<_> = sessions + .iter() + .filter(|s| s.app_password_name.as_deref() == Some(app_password_name)) + .collect(); + + let count = u64::try_from(to_delete.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + to_delete.iter().for_each(|session| { + self.delete_session_indexes(&mut batch, session); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn get_session_jtis_by_app_password( + &self, + did: &Did, + app_password_name: &str, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let sessions = self.collect_sessions_for_did(user_hash)?; + + Ok(sessions + .iter() + .filter(|s| s.app_password_name.as_deref() == Some(app_password_name)) + .map(|s| s.access_jti.clone()) + .collect()) + } + + pub fn check_refresh_token_used( + &self, + refresh_jti: &str, + ) -> Result, MetastoreError> { + let key = session_used_refresh_key(refresh_jti); + match self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => Ok(deserialize_used_refresh_value(&raw).map(SessionId::new)), + None => Ok(None), + } + } + + pub fn mark_refresh_token_used( + &self, + refresh_jti: &str, + session_id: SessionId, + ) -> Result { + let key = session_used_refresh_key(refresh_jti); + let existing = self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)?; + + match existing { + Some(_) => Ok(false), + None => { + let session = self.load_session_by_id(session_id.as_i32())?; + let expires_at_ms = session + .map(|s| s.refresh_expires_at_ms) + .unwrap_or(Utc::now().timestamp_millis().saturating_add(86_400_000)); + + self.auth + .insert( + key.as_slice(), + serialize_used_refresh_value(expires_at_ms, session_id.as_i32()), + ) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + } + } + + pub fn list_app_passwords( + &self, + user_id: Uuid, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_uuid(user_id)?; + let prefix = session_app_password_prefix(user_hash); + + let mut records: Vec = + self.auth + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = AppPasswordValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt app password"))?; + acc.push(self.value_to_app_password(&val)?); + Ok::<_, MetastoreError>(acc) + })?; + + records.sort_by_key(|r| std::cmp::Reverse(r.created_at)); + Ok(records) + } + + pub fn get_app_passwords_for_login( + &self, + user_id: Uuid, + ) -> Result, MetastoreError> { + let mut passwords = self.list_app_passwords(user_id)?; + passwords.truncate(20); + Ok(passwords) + } + + pub fn get_app_password_by_name( + &self, + user_id: Uuid, + name: &str, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_uuid(user_id)?; + let key = session_app_password_key(user_hash, name); + + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + AppPasswordValue::deserialize, + "corrupt app password", + )?; + + val.map(|v| self.value_to_app_password(&v)).transpose() + } + + pub fn create_app_password(&self, data: &AppPasswordCreate) -> Result { + let user_hash = self.resolve_user_hash_from_uuid(data.user_id)?; + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = AppPasswordValue { + id, + user_id: data.user_id, + name: data.name.clone(), + password_hash: data.password_hash.clone(), + created_at_ms: now_ms, + privilege: privilege_to_u8(data.privilege), + scopes: data.scopes.clone(), + created_by_controller_did: data + .created_by_controller_did + .as_ref() + .map(|d| d.to_string()), + }; + + let key = session_app_password_key(user_hash, &data.name); + self.auth + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall)?; + + Ok(id) + } + + pub fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result { + let user_hash = self.resolve_user_hash_from_uuid(user_id)?; + let key = session_app_password_key(user_hash, name); + + let exists = self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + .is_some(); + + match exists { + true => { + self.auth + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall)?; + Ok(1) + } + false => Ok(0), + } + } + + pub fn delete_app_passwords_by_controller( + &self, + did: &Did, + controller_did: &Did, + ) -> Result { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let user_uuid = self + .user_hashes + .get_uuid(&user_hash) + .ok_or(MetastoreError::InvalidInput("unknown did"))?; + let _ = user_uuid; + + let prefix = session_app_password_prefix(user_hash); + let controller_str = controller_did.to_string(); + + let keys_to_remove: Vec<_> = self + .auth + .prefix(prefix.as_slice()) + .filter_map(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().ok()?; + let val = AppPasswordValue::deserialize(&val_bytes)?; + match val.created_by_controller_did.as_deref() == Some(controller_str.as_str()) { + true => Some(key_bytes.to_vec()), + false => None, + } + }) + .collect(); + + let count = u64::try_from(keys_to_remove.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + keys_to_remove.iter().for_each(|key| { + batch.remove(&self.auth, key); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn get_last_reauth_at(&self, did: &Did) -> Result>, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let key = session_last_reauth_key(user_hash); + + match self + .auth + .get(key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) => { + Ok(deserialize_last_reauth_value(&raw).and_then(DateTime::from_timestamp_millis)) + } + None => Ok(None), + } + } + + pub fn update_last_reauth(&self, did: &Did) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let now = Utc::now(); + let key = session_last_reauth_key(user_hash); + + self.auth + .insert( + key.as_slice(), + serialize_last_reauth_value(now.timestamp_millis()), + ) + .map_err(MetastoreError::Fjall)?; + + Ok(now) + } + + pub fn get_session_mfa_status( + &self, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let mut sessions = self.collect_sessions_for_did(user_hash)?; + sessions.sort_by_key(|s| std::cmp::Reverse(s.created_at_ms)); + + let latest = match sessions.first() { + Some(s) => s, + None => return Ok(None), + }; + + let last_reauth_at = self.get_last_reauth_at(did)?; + + Ok(Some(SessionMfaStatus { + login_type: u8_to_login_type(latest.login_type).unwrap_or(LoginType::Modern), + mfa_verified: latest.mfa_verified, + last_reauth_at, + })) + } + + pub fn update_mfa_verified(&self, did: &Did) -> Result<(), MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + let sessions = self.collect_sessions_for_did(user_hash)?; + let now = Utc::now(); + let now_ms = now.timestamp_millis(); + + let mut batch = self.db.batch(); + sessions.iter().for_each(|session| { + let mut updated = session.clone(); + updated.mfa_verified = true; + updated.updated_at_ms = now_ms; + batch.insert( + &self.auth, + session_primary_key(updated.id).as_slice(), + updated.serialize(), + ); + }); + + let reauth_key = session_last_reauth_key(user_hash); + batch.insert( + &self.auth, + reauth_key.as_slice(), + serialize_last_reauth_value(now_ms), + ); + + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(()) + } + + pub fn get_app_password_hashes_by_did(&self, did: &Did) -> Result, MetastoreError> { + let user_hash = self.resolve_user_hash_from_did(did.as_str()); + match self.user_hashes.get_uuid(&user_hash) { + Some(_) => {} + None => return Ok(Vec::new()), + }; + + let prefix = session_app_password_prefix(user_hash); + + self.auth + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = AppPasswordValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt app password"))?; + acc.push(val.password_hash); + Ok::<_, MetastoreError>(acc) + }) + } + + pub fn refresh_session_atomic( + &self, + data: &SessionRefreshData, + ) -> Result { + let used_key = session_used_refresh_key(&data.old_refresh_jti); + let already_used = self + .auth + .get(used_key.as_slice()) + .map_err(MetastoreError::Fjall)?; + + if already_used.is_some() { + let mut batch = self.db.batch(); + let session = self.load_session_by_id(data.session_id.as_i32())?; + if let Some(s) = session { + self.delete_session_indexes(&mut batch, &s); + } + batch.commit().map_err(MetastoreError::Fjall)?; + return Ok(RefreshSessionResult::TokenAlreadyUsed); + } + + let mut session = match self.load_session_by_id(data.session_id.as_i32())? { + Some(s) => s, + None => return Ok(RefreshSessionResult::ConcurrentRefresh), + }; + + if session.refresh_jti != data.old_refresh_jti { + return Ok(RefreshSessionResult::ConcurrentRefresh); + } + + let user_hash = self.resolve_user_hash_from_did(&session.did); + let old_access_jti = session.access_jti.clone(); + let old_refresh_jti = session.refresh_jti.clone(); + + session.access_jti = data.new_access_jti.clone(); + session.refresh_jti = data.new_refresh_jti.clone(); + session.access_expires_at_ms = data.new_access_expires_at.timestamp_millis(); + session.refresh_expires_at_ms = data.new_refresh_expires_at.timestamp_millis(); + session.updated_at_ms = Utc::now().timestamp_millis(); + + let new_access_index = SessionIndexValue { + user_hash: user_hash.raw(), + session_id: session.id, + }; + let new_refresh_index = SessionIndexValue { + user_hash: user_hash.raw(), + session_id: session.id, + }; + + let mut batch = self.db.batch(); + + batch.insert( + &self.auth, + used_key.as_slice(), + serialize_used_refresh_value(session.refresh_expires_at_ms, session.id), + ); + + batch.remove( + &self.auth, + session_by_access_key(&old_access_jti).as_slice(), + ); + batch.remove( + &self.auth, + session_by_refresh_key(&old_refresh_jti).as_slice(), + ); + + batch.insert( + &self.auth, + session_primary_key(session.id).as_slice(), + session.serialize(), + ); + batch.insert( + &self.auth, + session_by_access_key(&data.new_access_jti).as_slice(), + new_access_index.serialize(session.refresh_expires_at_ms), + ); + batch.insert( + &self.auth, + session_by_refresh_key(&data.new_refresh_jti).as_slice(), + new_refresh_index.serialize(session.refresh_expires_at_ms), + ); + batch.insert( + &self.auth, + session_by_did_key(user_hash, session.id).as_slice(), + serialize_by_did_value(session.refresh_expires_at_ms), + ); + + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(RefreshSessionResult::Success) + } +} diff --git a/crates/tranquil-store/src/metastore/sessions.rs b/crates/tranquil-store/src/metastore/sessions.rs new file mode 100644 index 0000000..c6ad998 --- /dev/null +++ b/crates/tranquil-store/src/metastore/sessions.rs @@ -0,0 +1,459 @@ +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +use super::encoding::KeyBuilder; +use super::keys::{KeyTag, UserHash}; + +const SESSION_SCHEMA_VERSION: u8 = 1; +const APP_PASSWORD_SCHEMA_VERSION: u8 = 1; +const SESSION_INDEX_SCHEMA_VERSION: u8 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SessionTokenValue { + pub id: i32, + pub did: String, + pub access_jti: String, + pub refresh_jti: String, + pub access_expires_at_ms: i64, + pub refresh_expires_at_ms: i64, + pub login_type: u8, + pub mfa_verified: bool, + pub scope: Option, + pub controller_did: Option, + pub app_password_name: Option, + pub created_at_ms: i64, + pub updated_at_ms: i64, +} + +impl SessionTokenValue { + pub fn serialize(&self) -> Vec { + let ttl_bytes = u64::try_from(self.refresh_expires_at_ms) + .unwrap_or(0) + .to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("SessionTokenValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(SESSION_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + SESSION_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppPasswordValue { + pub id: uuid::Uuid, + pub user_id: uuid::Uuid, + pub name: String, + pub password_hash: String, + pub created_at_ms: i64, + pub privilege: u8, + pub scopes: Option, + pub created_by_controller_did: Option, +} + +impl AppPasswordValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("AppPasswordValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.push(APP_PASSWORD_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + APP_PASSWORD_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SessionIndexValue { + pub user_hash: u64, + pub session_id: i32, +} + +impl SessionIndexValue { + pub fn serialize(&self, expires_at_ms: i64) -> Vec { + let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("SessionIndexValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(SESSION_INDEX_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + SESSION_INDEX_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +pub fn login_type_to_u8(t: tranquil_db_traits::LoginType) -> u8 { + match t { + tranquil_db_traits::LoginType::Modern => 0, + tranquil_db_traits::LoginType::Legacy => 1, + } +} + +pub fn u8_to_login_type(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::LoginType::Modern), + 1 => Some(tranquil_db_traits::LoginType::Legacy), + _ => None, + } +} + +pub fn privilege_to_u8(p: tranquil_db_traits::AppPasswordPrivilege) -> u8 { + match p { + tranquil_db_traits::AppPasswordPrivilege::Standard => 0, + tranquil_db_traits::AppPasswordPrivilege::Privileged => 1, + } +} + +pub fn u8_to_privilege(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::AppPasswordPrivilege::Standard), + 1 => Some(tranquil_db_traits::AppPasswordPrivilege::Privileged), + _ => None, + } +} + +pub fn session_primary_key(session_id: i32) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_PRIMARY) + .raw(&session_id.to_be_bytes()) + .build() +} + +pub fn session_by_access_key(access_jti: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_BY_ACCESS) + .string(access_jti) + .build() +} + +pub fn session_by_refresh_key(refresh_jti: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_BY_REFRESH) + .string(refresh_jti) + .build() +} + +pub fn session_used_refresh_key(refresh_jti: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_USED_REFRESH) + .string(refresh_jti) + .build() +} + +pub fn session_app_password_key(user_hash: UserHash, name: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_APP_PASSWORD) + .u64(user_hash.raw()) + .string(name) + .build() +} + +pub fn session_app_password_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_APP_PASSWORD) + .u64(user_hash.raw()) + .build() +} + +pub fn session_by_did_key(user_hash: UserHash, session_id: i32) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_BY_DID) + .u64(user_hash.raw()) + .raw(&session_id.to_be_bytes()) + .build() +} + +pub fn session_by_did_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_BY_DID) + .u64(user_hash.raw()) + .build() +} + +pub fn session_last_reauth_key(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SESSION_LAST_REAUTH) + .u64(user_hash.raw()) + .build() +} + +pub fn session_id_counter_key() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::SESSION_ID_COUNTER).build() +} + +fn serialize_ttl_i32(expires_at_ms: i64, session_id: i32) -> Vec { + let mut buf = Vec::with_capacity(12); + buf.extend_from_slice(&u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes()); + buf.extend_from_slice(&session_id.to_be_bytes()); + buf +} + +fn deserialize_ttl_i32(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let arr: [u8; 4] = rest.try_into().ok()?; + Some(i32::from_be_bytes(arr)) +} + +pub fn serialize_used_refresh_value(expires_at_ms: i64, session_id: i32) -> Vec { + serialize_ttl_i32(expires_at_ms, session_id) +} + +pub fn deserialize_used_refresh_value(bytes: &[u8]) -> Option { + deserialize_ttl_i32(bytes) +} + +fn serialize_ttl_i64(timestamp_ms: i64) -> Vec { + let mut buf = Vec::with_capacity(16); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.extend_from_slice(×tamp_ms.to_be_bytes()); + buf +} + +fn deserialize_ttl_i64(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let arr: [u8; 8] = rest.try_into().ok()?; + Some(i64::from_be_bytes(arr)) +} + +pub fn serialize_last_reauth_value(timestamp_ms: i64) -> Vec { + serialize_ttl_i64(timestamp_ms) +} + +pub fn deserialize_last_reauth_value(bytes: &[u8]) -> Option { + deserialize_ttl_i64(bytes) +} + +pub fn serialize_by_did_value(expires_at_ms: i64) -> Vec { + u64::try_from(expires_at_ms) + .unwrap_or(0) + .to_be_bytes() + .to_vec() +} + +pub fn serialize_id_counter_value(counter: i32) -> Vec { + let mut buf = Vec::with_capacity(12); + buf.extend_from_slice(&0u64.to_be_bytes()); + buf.extend_from_slice(&counter.to_be_bytes()); + buf +} + +pub fn deserialize_id_counter_value(bytes: &[u8]) -> Option { + deserialize_ttl_i32(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn session_token_value_roundtrip() { + let val = SessionTokenValue { + id: 42, + did: "did:plc:test".to_owned(), + access_jti: "access_abc".to_owned(), + refresh_jti: "refresh_xyz".to_owned(), + access_expires_at_ms: 1700000060000, + refresh_expires_at_ms: 1700000600000, + login_type: 0, + mfa_verified: true, + scope: Some("atproto".to_owned()), + controller_did: None, + app_password_name: None, + created_at_ms: 1700000000000, + updated_at_ms: 1700000000000, + }; + let bytes = val.serialize(); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, u64::try_from(val.refresh_expires_at_ms).unwrap_or(0)); + assert_eq!(bytes[8], SESSION_SCHEMA_VERSION); + let decoded = SessionTokenValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn app_password_value_roundtrip() { + let val = AppPasswordValue { + id: uuid::Uuid::new_v4(), + user_id: uuid::Uuid::new_v4(), + name: "test-app".to_owned(), + password_hash: "hashed".to_owned(), + created_at_ms: 1700000000000, + privilege: 0, + scopes: None, + created_by_controller_did: None, + }; + let bytes = val.serialize(); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, 0); + assert_eq!(bytes[8], APP_PASSWORD_SCHEMA_VERSION); + let decoded = AppPasswordValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn session_index_value_roundtrip() { + let val = SessionIndexValue { + user_hash: 0xDEAD_BEEF, + session_id: 99, + }; + let expires_at_ms = 1700000600000i64; + let bytes = val.serialize(expires_at_ms); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, u64::try_from(expires_at_ms).unwrap_or(0)); + let decoded = SessionIndexValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn used_refresh_value_roundtrip() { + let session_id = 7; + let expires_at_ms = 1700000600000i64; + let bytes = serialize_used_refresh_value(expires_at_ms, session_id); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, u64::try_from(expires_at_ms).unwrap_or(0)); + assert_eq!(deserialize_used_refresh_value(&bytes), Some(session_id)); + } + + #[test] + fn last_reauth_value_roundtrip() { + let ts = 1700000000000i64; + let bytes = serialize_last_reauth_value(ts); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, 0); + assert_eq!(deserialize_last_reauth_value(&bytes), Some(ts)); + } + + #[test] + fn id_counter_value_roundtrip() { + let counter = 123; + let bytes = serialize_id_counter_value(counter); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, 0); + assert_eq!(deserialize_id_counter_value(&bytes), Some(counter)); + } + + #[test] + fn session_primary_key_roundtrip() { + use super::super::encoding::KeyReader; + let key = session_primary_key(42); + let mut reader = KeyReader::new(&key); + assert_eq!(reader.tag(), Some(KeyTag::SESSION_PRIMARY.raw())); + let remaining = reader.remaining(); + let arr: [u8; 4] = remaining.try_into().unwrap(); + assert_eq!(i32::from_be_bytes(arr), 42); + } + + #[test] + fn session_by_access_key_roundtrip() { + use super::super::encoding::KeyReader; + let key = session_by_access_key("jti_abc"); + let mut reader = KeyReader::new(&key); + assert_eq!(reader.tag(), Some(KeyTag::SESSION_BY_ACCESS.raw())); + assert_eq!(reader.string(), Some("jti_abc".to_owned())); + assert!(reader.is_empty()); + } + + #[test] + fn session_by_did_prefix_is_prefix_of_key() { + let uh = UserHash::from_did("did:plc:test"); + let prefix = session_by_did_prefix(uh); + let key = session_by_did_key(uh, 5); + assert!(key.starts_with(prefix.as_slice())); + } + + #[test] + fn app_password_prefix_is_prefix_of_key() { + let uh = UserHash::from_did("did:plc:test"); + let prefix = session_app_password_prefix(uh); + let key = session_app_password_key(uh, "my-app"); + assert!(key.starts_with(prefix.as_slice())); + } + + #[test] + fn deserialize_unknown_version_returns_none() { + let val = SessionTokenValue { + id: 1, + did: String::new(), + access_jti: String::new(), + refresh_jti: String::new(), + access_expires_at_ms: 0, + refresh_expires_at_ms: 0, + login_type: 0, + mfa_verified: false, + scope: None, + controller_did: None, + app_password_name: None, + created_at_ms: 0, + updated_at_ms: 0, + }; + let mut bytes = val.serialize(); + bytes[8] = 99; + assert!(SessionTokenValue::deserialize(&bytes).is_none()); + } + + #[test] + fn deserialize_too_short_returns_none() { + assert!(SessionTokenValue::deserialize(&[0; 8]).is_none()); + assert!(SessionTokenValue::deserialize(&[0; 7]).is_none()); + assert!(AppPasswordValue::deserialize(&[0; 8]).is_none()); + assert!(SessionIndexValue::deserialize(&[0; 8]).is_none()); + } + + #[test] + fn login_type_conversion_roundtrip() { + assert_eq!( + u8_to_login_type(login_type_to_u8(tranquil_db_traits::LoginType::Modern)), + Some(tranquil_db_traits::LoginType::Modern) + ); + assert_eq!( + u8_to_login_type(login_type_to_u8(tranquil_db_traits::LoginType::Legacy)), + Some(tranquil_db_traits::LoginType::Legacy) + ); + assert_eq!(u8_to_login_type(99), None); + } + + #[test] + fn privilege_conversion_roundtrip() { + assert_eq!( + u8_to_privilege(privilege_to_u8( + tranquil_db_traits::AppPasswordPrivilege::Standard + )), + Some(tranquil_db_traits::AppPasswordPrivilege::Standard) + ); + assert_eq!( + u8_to_privilege(privilege_to_u8( + tranquil_db_traits::AppPasswordPrivilege::Privileged + )), + Some(tranquil_db_traits::AppPasswordPrivilege::Privileged) + ); + assert_eq!(u8_to_privilege(99), None); + } +} diff --git a/crates/tranquil-store/src/metastore/sso_ops.rs b/crates/tranquil-store/src/metastore/sso_ops.rs new file mode 100644 index 0000000..fc03ba1 --- /dev/null +++ b/crates/tranquil-store/src/metastore/sso_ops.rs @@ -0,0 +1,482 @@ +use chrono::{DateTime, Utc}; +use fjall::{Database, Keyspace}; +use uuid::Uuid; + +use super::MetastoreError; +use super::keys::UserHash; +use super::scan::point_lookup; +use super::sso_schema::{ + ExternalIdentityValue, PendingRegistrationValue, SsoAuthStateValue, auth_state_key, + auth_state_prefix, by_id_key, by_provider_key, identity_key, identity_user_prefix, + pending_reg_key, pending_reg_prefix, provider_to_u8, u8_to_provider, +}; + +use tranquil_db_traits::{ + ExternalEmail, ExternalIdentity, ExternalUserId, ExternalUsername, SsoAction, SsoAuthState, + SsoPendingRegistration, SsoProviderType, +}; +use tranquil_types::Did; + +pub struct SsoOps { + db: Database, + indexes: Keyspace, +} + +impl SsoOps { + pub fn new(db: Database, indexes: Keyspace) -> Self { + Self { db, indexes } + } + + fn value_to_identity(v: &ExternalIdentityValue) -> Result { + Ok(ExternalIdentity { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid did in sso identity"))?, + provider: u8_to_provider(v.provider) + .ok_or(MetastoreError::CorruptData("unknown sso provider"))?, + provider_user_id: ExternalUserId::new(v.provider_user_id.clone()), + provider_username: v + .provider_username + .as_ref() + .map(|s| ExternalUsername::new(s.clone())), + provider_email: v + .provider_email + .as_ref() + .map(|s| ExternalEmail::new(s.clone())), + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + updated_at: DateTime::from_timestamp_millis(v.updated_at_ms).unwrap_or_default(), + last_login_at: v.last_login_at_ms.and_then(DateTime::from_timestamp_millis), + }) + } + + fn value_to_auth_state(v: &SsoAuthStateValue) -> Result { + Ok(SsoAuthState { + state: v.state.clone(), + request_uri: v.request_uri.clone(), + provider: u8_to_provider(v.provider) + .ok_or(MetastoreError::CorruptData("unknown sso provider"))?, + action: SsoAction::parse(&v.action) + .ok_or(MetastoreError::CorruptData("unknown sso action"))?, + nonce: v.nonce.clone(), + code_verifier: v.code_verifier.clone(), + did: v + .did + .as_ref() + .map(|d| Did::new(d.clone())) + .transpose() + .map_err(|_| MetastoreError::CorruptData("invalid did in sso auth state"))?, + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), + }) + } + + fn value_to_pending_reg( + v: &PendingRegistrationValue, + ) -> Result { + Ok(SsoPendingRegistration { + token: v.token.clone(), + request_uri: v.request_uri.clone(), + provider: u8_to_provider(v.provider) + .ok_or(MetastoreError::CorruptData("unknown sso provider"))?, + provider_user_id: ExternalUserId::new(v.provider_user_id.clone()), + provider_username: v + .provider_username + .as_ref() + .map(|s| ExternalUsername::new(s.clone())), + provider_email: v + .provider_email + .as_ref() + .map(|s| ExternalEmail::new(s.clone())), + provider_email_verified: v.provider_email_verified, + created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), + }) + } + + pub fn create_external_identity( + &self, + did: &Did, + provider: SsoProviderType, + provider_user_id: &str, + provider_username: Option<&str>, + provider_email: Option<&str>, + ) -> Result { + let user_hash = UserHash::from_did(did.as_str()); + let prov_u8 = provider_to_u8(provider); + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = ExternalIdentityValue { + id, + did: did.to_string(), + provider: prov_u8, + provider_user_id: provider_user_id.to_owned(), + provider_username: provider_username.map(str::to_owned), + provider_email: provider_email.map(str::to_owned), + created_at_ms: now_ms, + updated_at_ms: now_ms, + last_login_at_ms: None, + }; + + let primary = identity_key(user_hash, prov_u8, provider_user_id); + let provider_index = by_provider_key(prov_u8, provider_user_id); + let id_index = by_id_key(id); + + let provider_index_val = { + let mut buf = Vec::with_capacity(8 + 16); + buf.extend_from_slice(&user_hash.raw().to_be_bytes()); + buf.extend_from_slice(id.as_bytes()); + buf + }; + + let id_index_val = { + let puid_bytes = provider_user_id.as_bytes(); + let mut buf = Vec::with_capacity(8 + 1 + puid_bytes.len()); + buf.extend_from_slice(&user_hash.raw().to_be_bytes()); + buf.push(prov_u8); + buf.extend_from_slice(puid_bytes); + buf + }; + + let mut batch = self.db.batch(); + batch.insert(&self.indexes, primary.as_slice(), value.serialize()); + batch.insert(&self.indexes, provider_index.as_slice(), provider_index_val); + batch.insert(&self.indexes, id_index.as_slice(), id_index_val); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(id) + } + + pub fn get_external_identity_by_provider( + &self, + provider: SsoProviderType, + provider_user_id: &str, + ) -> Result, MetastoreError> { + let prov_u8 = provider_to_u8(provider); + let index = by_provider_key(prov_u8, provider_user_id); + + let index_val = match self + .indexes + .get(index.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(v) => v, + None => return Ok(None), + }; + + let (user_hash_raw, _id_bytes) = index_val.as_ref().split_at(8); + let user_hash = + UserHash::from_raw(u64::from_be_bytes(user_hash_raw.try_into().map_err( + |_| MetastoreError::CorruptData("corrupt sso by_provider index"), + )?)); + + let primary = identity_key(user_hash, prov_u8, provider_user_id); + let val: Option = point_lookup( + &self.indexes, + primary.as_slice(), + ExternalIdentityValue::deserialize, + "corrupt sso identity", + )?; + + val.map(|v| Self::value_to_identity(&v)).transpose() + } + + pub fn get_external_identities_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = UserHash::from_did(did.as_str()); + let prefix = identity_user_prefix(user_hash); + + self.indexes + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = ExternalIdentityValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt sso identity"))?; + acc.push(Self::value_to_identity(&val)?); + Ok::<_, MetastoreError>(acc) + }) + } + + pub fn update_external_identity_login( + &self, + id: Uuid, + provider_username: Option<&str>, + provider_email: Option<&str>, + ) -> Result<(), MetastoreError> { + let id_idx = by_id_key(id); + let id_val = match self + .indexes + .get(id_idx.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(v) => v, + None => return Ok(()), + }; + + let raw = id_val.as_ref(); + if raw.len() < 9 { + return Err(MetastoreError::CorruptData("corrupt sso by_id index")); + } + let user_hash = UserHash::from_raw(u64::from_be_bytes( + raw[..8] + .try_into() + .map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?, + )); + let provider = raw[8]; + let provider_user_id = std::str::from_utf8(&raw[9..]) + .map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?; + + let primary = identity_key(user_hash, provider, provider_user_id); + let existing: Option = point_lookup( + &self.indexes, + primary.as_slice(), + ExternalIdentityValue::deserialize, + "corrupt sso identity", + )?; + + match existing { + Some(mut val) => { + val.provider_username = provider_username.map(str::to_owned); + val.provider_email = provider_email.map(str::to_owned); + let now_ms = Utc::now().timestamp_millis(); + val.last_login_at_ms = Some(now_ms); + val.updated_at_ms = now_ms; + self.indexes + .insert(primary.as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(()) + } + None => Ok(()), + } + } + + pub fn delete_external_identity(&self, id: Uuid, _did: &Did) -> Result { + let id_idx = by_id_key(id); + let id_val = match self + .indexes + .get(id_idx.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(v) => v, + None => return Ok(false), + }; + + let raw = id_val.as_ref(); + if raw.len() < 9 { + return Err(MetastoreError::CorruptData("corrupt sso by_id index")); + } + let user_hash = UserHash::from_raw(u64::from_be_bytes( + raw[..8] + .try_into() + .map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?, + )); + let provider = raw[8]; + let provider_user_id = std::str::from_utf8(&raw[9..]) + .map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?; + + let primary = identity_key(user_hash, provider, provider_user_id); + let provider_idx = by_provider_key(provider, provider_user_id); + + let mut batch = self.db.batch(); + batch.remove(&self.indexes, primary.as_slice()); + batch.remove(&self.indexes, provider_idx.as_slice()); + batch.remove(&self.indexes, id_idx.as_slice()); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(true) + } + + #[allow(clippy::too_many_arguments)] + pub fn create_sso_auth_state( + &self, + state: &str, + request_uri: &str, + provider: SsoProviderType, + action: SsoAction, + nonce: Option<&str>, + code_verifier: Option<&str>, + did: Option<&Did>, + ) -> Result<(), MetastoreError> { + let now_ms = Utc::now().timestamp_millis(); + let expires_at_ms = now_ms.saturating_add(600_000); + + let value = SsoAuthStateValue { + state: state.to_owned(), + request_uri: request_uri.to_owned(), + provider: provider_to_u8(provider), + action: action.as_str().to_owned(), + nonce: nonce.map(str::to_owned), + code_verifier: code_verifier.map(str::to_owned), + did: did.map(|d| d.to_string()), + created_at_ms: now_ms, + expires_at_ms, + }; + + let key = auth_state_key(state); + self.indexes + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(()) + } + + pub fn consume_sso_auth_state( + &self, + state: &str, + ) -> Result, MetastoreError> { + let key = auth_state_key(state); + + let val: Option = point_lookup( + &self.indexes, + key.as_slice(), + SsoAuthStateValue::deserialize, + "corrupt sso auth state", + )?; + + match val { + Some(v) => { + self.indexes + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall)?; + let now_ms = Utc::now().timestamp_millis(); + match v.expires_at_ms < now_ms { + true => Ok(None), + false => Self::value_to_auth_state(&v).map(Some), + } + } + None => Ok(None), + } + } + + pub fn cleanup_expired_sso_auth_states(&self) -> Result { + let prefix = auth_state_prefix(); + let now_ms = Utc::now().timestamp_millis(); + let mut count = 0u64; + let mut batch = self.db.batch(); + + self.indexes + .prefix(prefix.as_slice()) + .try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = SsoAuthStateValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt sso auth state"))?; + if val.expires_at_ms < now_ms { + batch.remove(&self.indexes, key_bytes.as_ref()); + count = count.saturating_add(1); + } + Ok::<_, MetastoreError>(()) + })?; + + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + + #[allow(clippy::too_many_arguments)] + pub fn create_pending_registration( + &self, + token: &str, + request_uri: &str, + provider: SsoProviderType, + provider_user_id: &str, + provider_username: Option<&str>, + provider_email: Option<&str>, + provider_email_verified: bool, + ) -> Result<(), MetastoreError> { + let now_ms = Utc::now().timestamp_millis(); + let expires_at_ms = now_ms.saturating_add(600_000); + + let value = PendingRegistrationValue { + token: token.to_owned(), + request_uri: request_uri.to_owned(), + provider: provider_to_u8(provider), + provider_user_id: provider_user_id.to_owned(), + provider_username: provider_username.map(str::to_owned), + provider_email: provider_email.map(str::to_owned), + provider_email_verified, + created_at_ms: now_ms, + expires_at_ms, + }; + + let key = pending_reg_key(token); + self.indexes + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(()) + } + + pub fn get_pending_registration( + &self, + token: &str, + ) -> Result, MetastoreError> { + let key = pending_reg_key(token); + let val: Option = point_lookup( + &self.indexes, + key.as_slice(), + PendingRegistrationValue::deserialize, + "corrupt sso pending registration", + )?; + + match val { + Some(v) => { + let now_ms = Utc::now().timestamp_millis(); + match v.expires_at_ms < now_ms { + true => Ok(None), + false => Self::value_to_pending_reg(&v).map(Some), + } + } + None => Ok(None), + } + } + + pub fn consume_pending_registration( + &self, + token: &str, + ) -> Result, MetastoreError> { + let key = pending_reg_key(token); + let val: Option = point_lookup( + &self.indexes, + key.as_slice(), + PendingRegistrationValue::deserialize, + "corrupt sso pending registration", + )?; + + match val { + Some(v) => { + self.indexes + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall)?; + let now_ms = Utc::now().timestamp_millis(); + match v.expires_at_ms < now_ms { + true => Ok(None), + false => Self::value_to_pending_reg(&v).map(Some), + } + } + None => Ok(None), + } + } + + pub fn cleanup_expired_pending_registrations(&self) -> Result { + let prefix = pending_reg_prefix(); + let now_ms = Utc::now().timestamp_millis(); + let mut count = 0u64; + let mut batch = self.db.batch(); + + self.indexes + .prefix(prefix.as_slice()) + .try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = PendingRegistrationValue::deserialize(&val_bytes).ok_or( + MetastoreError::CorruptData("corrupt sso pending registration"), + )?; + if val.expires_at_ms < now_ms { + batch.remove(&self.indexes, key_bytes.as_ref()); + count = count.saturating_add(1); + } + Ok::<_, MetastoreError>(()) + })?; + + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } +} diff --git a/crates/tranquil-store/src/metastore/sso_schema.rs b/crates/tranquil-store/src/metastore/sso_schema.rs new file mode 100644 index 0000000..402f111 --- /dev/null +++ b/crates/tranquil-store/src/metastore/sso_schema.rs @@ -0,0 +1,227 @@ +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +use super::encoding::KeyBuilder; +use super::keys::{KeyTag, UserHash}; + +const IDENTITY_SCHEMA_VERSION: u8 = 1; +const AUTH_STATE_SCHEMA_VERSION: u8 = 1; +const PENDING_REG_SCHEMA_VERSION: u8 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ExternalIdentityValue { + pub id: uuid::Uuid, + pub did: String, + pub provider: u8, + pub provider_user_id: String, + pub provider_username: Option, + pub provider_email: Option, + pub created_at_ms: i64, + pub updated_at_ms: i64, + pub last_login_at_ms: Option, +} + +impl ExternalIdentityValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("ExternalIdentityValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(IDENTITY_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + IDENTITY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SsoAuthStateValue { + pub state: String, + pub request_uri: String, + pub provider: u8, + pub action: String, + pub nonce: Option, + pub code_verifier: Option, + pub did: Option, + pub created_at_ms: i64, + pub expires_at_ms: i64, +} + +impl SsoAuthStateValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("SsoAuthStateValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(AUTH_STATE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + AUTH_STATE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PendingRegistrationValue { + pub token: String, + pub request_uri: String, + pub provider: u8, + pub provider_user_id: String, + pub provider_username: Option, + pub provider_email: Option, + pub provider_email_verified: bool, + pub created_at_ms: i64, + pub expires_at_ms: i64, +} + +impl PendingRegistrationValue { + pub fn serialize(&self) -> Vec { + let payload = postcard::to_allocvec(self) + .expect("PendingRegistrationValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(PENDING_REG_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + PENDING_REG_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +pub fn provider_to_u8(p: tranquil_db_traits::SsoProviderType) -> u8 { + match p { + tranquil_db_traits::SsoProviderType::Github => 0, + tranquil_db_traits::SsoProviderType::Discord => 1, + tranquil_db_traits::SsoProviderType::Google => 2, + tranquil_db_traits::SsoProviderType::Gitlab => 3, + tranquil_db_traits::SsoProviderType::Oidc => 4, + tranquil_db_traits::SsoProviderType::Apple => 5, + } +} + +pub fn u8_to_provider(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::SsoProviderType::Github), + 1 => Some(tranquil_db_traits::SsoProviderType::Discord), + 2 => Some(tranquil_db_traits::SsoProviderType::Google), + 3 => Some(tranquil_db_traits::SsoProviderType::Gitlab), + 4 => Some(tranquil_db_traits::SsoProviderType::Oidc), + 5 => Some(tranquil_db_traits::SsoProviderType::Apple), + _ => None, + } +} + +pub fn identity_key( + user_hash: UserHash, + provider: u8, + provider_user_id: &str, +) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SSO_IDENTITY) + .u64(user_hash.raw()) + .raw(&[provider]) + .string(provider_user_id) + .build() +} + +pub fn identity_user_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SSO_IDENTITY) + .u64(user_hash.raw()) + .build() +} + +pub fn by_provider_key(provider: u8, provider_user_id: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SSO_BY_PROVIDER) + .raw(&[provider]) + .string(provider_user_id) + .build() +} + +pub fn by_id_key(id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SSO_BY_ID) + .fixed(id.as_bytes()) + .build() +} + +pub fn auth_state_key(state: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SSO_AUTH_STATE) + .string(state) + .build() +} + +pub fn auth_state_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::SSO_AUTH_STATE).build() +} + +pub fn pending_reg_key(token: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::SSO_PENDING_REG) + .string(token) + .build() +} + +pub fn pending_reg_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::SSO_PENDING_REG).build() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identity_value_roundtrip() { + let val = ExternalIdentityValue { + id: uuid::Uuid::new_v4(), + did: "did:plc:test".to_owned(), + provider: 0, + provider_user_id: "12345".to_owned(), + provider_username: Some("user".to_owned()), + provider_email: Some("user@example.com".to_owned()), + created_at_ms: 1700000000000, + updated_at_ms: 1700000000000, + last_login_at_ms: None, + }; + let bytes = val.serialize(); + assert_eq!(bytes[0], IDENTITY_SCHEMA_VERSION); + let decoded = ExternalIdentityValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn auth_state_value_roundtrip() { + let val = SsoAuthStateValue { + state: "random-state".to_owned(), + request_uri: "urn:ietf:params:oauth:request_uri:abc".to_owned(), + provider: 0, + action: "login".to_owned(), + nonce: None, + code_verifier: None, + did: None, + created_at_ms: 1700000000000, + expires_at_ms: 1700000600000, + }; + let bytes = val.serialize(); + let decoded = SsoAuthStateValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } +} diff --git a/crates/tranquil-store/src/metastore/user_ops.rs b/crates/tranquil-store/src/metastore/user_ops.rs new file mode 100644 index 0000000..755e785 --- /dev/null +++ b/crates/tranquil-store/src/metastore/user_ops.rs @@ -0,0 +1,3099 @@ +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use fjall::{Database, Keyspace}; +use uuid::Uuid; + +use super::MetastoreError; +use super::infra_schema::{channel_to_u8, u8_to_channel}; +use super::keys::UserHash; +use super::repo_meta::{RepoMetaValue, RepoStatus, handle_key, repo_meta_key}; +use super::repo_ops::cid_link_to_bytes; +use super::scan::{count_prefix, delete_all_by_prefix, point_lookup}; +use super::sessions::{SessionIndexValue, session_by_access_key}; +use super::user_hash::UserHashMap; +use super::users::{ + BackupCodeValue, DidWebOverridesValue, HandleReservationValue, PasskeyIndexValue, PasskeyValue, + RecoveryTokenValue, ResetCodeValue, TotpValue, UserValue, WebauthnChallengeValue, + account_type_to_u8, backup_code_key, backup_code_prefix, challenge_type_to_u8, + did_web_overrides_key, discord_lookup_key, handle_reservation_key, handle_reservation_prefix, + passkey_by_cred_key, passkey_key, passkey_prefix, recovery_token_key, reset_code_key, + telegram_lookup_key, totp_key, u8_to_account_type, user_by_email_key, user_by_handle_key, + user_primary_key, user_primary_prefix, webauthn_challenge_key, +}; + +use tranquil_db_traits::{ + AccountSearchResult, AccountType, ChannelVerificationStatus, CommsChannel, + CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput, + CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult, + CreateSsoAccountInput, DidWebOverrides, MigrationReactivationError, MigrationReactivationInput, + NotificationPrefs, OAuthTokenWithUser, PasswordResetResult, ReactivatedAccountInfo, + RecoverPasskeyAccountInput, RecoverPasskeyAccountResult, ScheduledDeletionAccount, + StoredBackupCode, StoredPasskey, TotpRecord, TotpRecordState, User2faStatus, UserAuthInfo, + UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, UserForDeletion, + UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, UserForPasskeySetup, + UserForRecovery, UserForVerification, UserIdAndHandle, UserIdAndPasswordHash, + UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId, UserLegacyLoginPref, + UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo, UserResendVerification, + UserResetCodeInfo, UserRow, UserSessionInfo, UserStatus, UserVerificationInfo, UserWithKey, + WebauthnChallengeType, +}; +use tranquil_types::{CidLink, Did, Handle}; + +pub struct UserOps { + db: Database, + users: Keyspace, + repo_data: Keyspace, + auth: Keyspace, + user_hashes: Arc, +} + +impl UserOps { + pub fn new( + db: Database, + users: Keyspace, + repo_data: Keyspace, + auth: Keyspace, + user_hashes: Arc, + ) -> Self { + Self { + db, + users, + repo_data, + auth, + user_hashes, + } + } + + fn resolve_hash(&self, did: &str) -> UserHash { + UserHash::from_did(did) + } + + fn resolve_hash_from_uuid(&self, user_id: Uuid) -> Result { + self.user_hashes + .get(&user_id) + .ok_or(MetastoreError::InvalidInput("unknown user_id")) + } + + fn load_user(&self, user_hash: UserHash) -> Result, MetastoreError> { + point_lookup( + &self.users, + user_primary_key(user_hash).as_slice(), + UserValue::deserialize, + "corrupt user value", + ) + } + + fn load_user_by_did(&self, did: &str) -> Result, MetastoreError> { + self.load_user(self.resolve_hash(did)) + } + + fn save_user(&self, user_hash: UserHash, val: &UserValue) -> Result<(), MetastoreError> { + self.users + .insert(user_primary_key(user_hash).as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall) + } + + fn load_by_handle(&self, handle: &str) -> Result, MetastoreError> { + let idx_key = user_by_handle_key(handle); + match self + .users + .get(idx_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) if raw.len() >= 8 => { + let hash_raw = u64::from_be_bytes(raw[..8].try_into().unwrap()); + self.load_user(UserHash::from_raw(hash_raw)) + } + _ => Ok(None), + } + } + + fn load_by_email(&self, email: &str) -> Result, MetastoreError> { + let idx_key = user_by_email_key(email); + match self + .users + .get(idx_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) if raw.len() >= 8 => { + let hash_raw = u64::from_be_bytes(raw[..8].try_into().unwrap()); + self.load_user(UserHash::from_raw(hash_raw)) + } + _ => Ok(None), + } + } + + fn load_by_identifier(&self, identifier: &str) -> Result, MetastoreError> { + match identifier.contains('@') { + true => self.load_by_email(identifier).and_then(|opt| match opt { + Some(v) => Ok(Some(v)), + None => self.load_by_handle(identifier), + }), + false => self.load_by_handle(identifier).and_then(|opt| match opt { + Some(v) => Ok(Some(v)), + None => self.load_by_email(identifier), + }), + } + } + + fn mutate_user(&self, user_hash: UserHash, f: F) -> Result + where + F: FnOnce(&mut UserValue), + { + match self.load_user(user_hash)? { + Some(mut val) => { + f(&mut val); + self.save_user(user_hash, &val)?; + Ok(true) + } + None => Ok(false), + } + } + + fn mutate_user_by_uuid(&self, user_id: Uuid, f: F) -> Result + where + F: FnOnce(&mut UserValue), + { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + self.mutate_user(user_hash, f) + } + + fn channel_verification(val: &UserValue) -> ChannelVerificationStatus { + ChannelVerificationStatus::from_db_row( + val.email_verified, + val.discord_verified, + val.telegram_verified, + val.signal_verified, + ) + } + + fn comms_channel(val: &UserValue) -> CommsChannel { + val.preferred_comms_channel + .and_then(u8_to_channel) + .unwrap_or(CommsChannel::Email) + } + + fn to_user_row(val: &UserValue) -> Result { + Ok(UserRow { + id: val.id, + did: Did::new(val.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + handle: Handle::new(val.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: val.email.clone(), + created_at: DateTime::from_timestamp_millis(val.created_at_ms).unwrap_or_default(), + deactivated_at: val + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: val.takedown_ref.clone(), + is_admin: val.is_admin, + }) + } + + fn to_stored_passkey(pv: &PasskeyValue) -> Result { + Ok(StoredPasskey { + id: pv.id, + did: Did::new(pv.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid passkey did"))?, + credential_id: pv.credential_id.clone(), + public_key: pv.public_key.clone(), + sign_count: pv.sign_count, + created_at: DateTime::from_timestamp_millis(pv.created_at_ms).unwrap_or_default(), + last_used: pv.last_used_at_ms.and_then(DateTime::from_timestamp_millis), + friendly_name: pv.friendly_name.clone(), + aaguid: pv.aaguid.clone(), + transports: pv.transports.clone(), + }) + } + + fn is_first_account(&self) -> Result { + let prefix = user_primary_prefix(); + match self.users.prefix(prefix.as_slice()).next() { + Some(guard) => { + guard.into_inner().map_err(MetastoreError::Fjall)?; + Ok(false) + } + None => Ok(true), + } + } + + pub fn get_by_did(&self, did: &Did) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| Self::to_user_row(&v)) + .transpose() + } + + pub fn get_by_handle(&self, handle: &Handle) -> Result, MetastoreError> { + self.load_by_handle(handle.as_str())? + .map(|v| Self::to_user_row(&v)) + .transpose() + } + + pub fn get_with_key_by_did(&self, did: &Did) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserWithKey { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: v.email.clone(), + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: v.takedown_ref.clone(), + is_admin: v.is_admin, + key_bytes: v.key_bytes.clone(), + encryption_version: Some(v.encryption_version), + }) + }) + .transpose() + } + + pub fn get_status_by_did(&self, did: &Did) -> Result, MetastoreError> { + Ok(self.load_user_by_did(did.as_str())?.map(|v| UserStatus { + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: v.takedown_ref.clone(), + is_admin: v.is_admin, + })) + } + + pub fn count_users(&self) -> Result { + count_prefix(&self.users, user_primary_prefix().as_slice()) + } + + pub fn get_session_access_expiry( + &self, + did: &Did, + access_jti: &str, + ) -> Result>, MetastoreError> { + let index_key = session_by_access_key(access_jti); + let index_val: Option = point_lookup( + &self.auth, + index_key.as_slice(), + SessionIndexValue::deserialize, + "corrupt session access index", + )?; + + match index_val { + Some(idx) => { + let session_key = super::sessions::session_primary_key(idx.session_id); + let session: Option = point_lookup( + &self.auth, + session_key.as_slice(), + super::sessions::SessionTokenValue::deserialize, + "corrupt session token", + )?; + match session.filter(|s| s.did == did.as_str()) { + Some(s) => Ok(DateTime::from_timestamp_millis(s.access_expires_at_ms)), + None => Ok(None), + } + } + None => Ok(None), + } + } + + pub fn get_oauth_token_with_user( + &self, + token_id: &str, + ) -> Result, MetastoreError> { + let index_key = super::oauth_schema::oauth_token_by_id_key(token_id); + let index_val: Option = point_lookup( + &self.auth, + index_key.as_slice(), + super::oauth_schema::TokenIndexValue::deserialize, + "corrupt oauth token index", + )?; + + let idx = match index_val { + Some(idx) => idx, + None => return Ok(None), + }; + + let uh = UserHash::from_raw(idx.user_hash); + let token_key = super::oauth_schema::oauth_token_key(uh, idx.family_id); + let token: Option = point_lookup( + &self.auth, + token_key.as_slice(), + super::oauth_schema::OAuthTokenValue::deserialize, + "corrupt oauth token", + )?; + + let token = match token { + Some(t) => t, + None => return Ok(None), + }; + + let user = self.load_user(uh)?; + match user { + Some(u) => Ok(Some(OAuthTokenWithUser { + did: Did::new(token.did) + .map_err(|_| MetastoreError::CorruptData("invalid oauth token did"))?, + expires_at: DateTime::from_timestamp_millis(token.expires_at_ms) + .unwrap_or_default(), + deactivated_at: u + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: u.takedown_ref.clone(), + is_admin: u.is_admin, + key_bytes: Some(u.key_bytes.clone()), + encryption_version: Some(u.encryption_version), + })), + None => Ok(None), + } + } + + pub fn get_user_info_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + Ok(self + .load_user_by_did(did.as_str())? + .map(|v| UserInfoForAuth { + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: v.takedown_ref.clone(), + is_admin: v.is_admin, + key_bytes: Some(v.key_bytes.clone()), + encryption_version: Some(v.encryption_version), + })) + } + + pub fn get_any_admin_user_id(&self) -> Result, MetastoreError> { + let prefix = user_primary_prefix(); + self.users + .prefix(prefix.as_slice()) + .map(|guard| -> Result, MetastoreError> { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = UserValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt user value"))?; + Ok(val.is_admin.then_some(val.id)) + }) + .filter_map(Result::transpose) + .next() + .transpose() + } + + pub fn set_invites_disabled(&self, did: &Did, disabled: bool) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + self.mutate_user(user_hash, |u| { + u.invites_disabled = disabled; + }) + } + + pub fn search_accounts( + &self, + cursor_did: Option<&Did>, + email_filter: Option<&str>, + handle_filter: Option<&str>, + limit: i64, + ) -> Result, MetastoreError> { + let prefix = user_primary_prefix(); + let limit = usize::try_from(limit).unwrap_or(0); + let cursor_hash = cursor_did.map(|d| self.resolve_hash(d.as_str())); + + self.users + .prefix(prefix.as_slice()) + .map( + |guard| -> Result, MetastoreError> { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = UserValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt user value"))?; + + let val_hash = UserHash::from_did(&val.did); + if cursor_hash.is_some_and(|cursor| val_hash.raw() <= cursor.raw()) { + return Ok(None); + } + + let email_match = email_filter.map_or(true, |f| { + val.email.as_deref().map_or(false, |e| e.contains(f)) + }); + let handle_match = handle_filter.map_or(true, |f| val.handle.contains(f)); + + match email_match && handle_match { + true => Ok(Some(AccountSearchResult { + did: Did::new(val.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + handle: Handle::new(val.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: val.email.clone(), + created_at: DateTime::from_timestamp_millis(val.created_at_ms) + .unwrap_or_default(), + email_verified: val.email_verified, + deactivated_at: val + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + invites_disabled: Some(val.invites_disabled), + })), + false => Ok(None), + } + }, + ) + .filter_map(Result::transpose) + .take(limit) + .collect() + } + + pub fn get_auth_info_by_did(&self, did: &Did) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserAuthInfo { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + password_hash: v.password_hash.clone(), + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: v.takedown_ref.clone(), + channel_verification: Self::channel_verification(&v), + }) + }) + .transpose() + } + + pub fn get_by_email(&self, email: &str) -> Result, MetastoreError> { + self.load_by_email(email)? + .map(|v| { + Ok(UserForVerification { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + email: v.email.clone(), + email_verified: v.email_verified, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + }) + }) + .transpose() + } + + pub fn get_login_check_by_handle_or_email( + &self, + identifier: &str, + ) -> Result, MetastoreError> { + self.load_by_identifier(identifier)? + .map(|v| { + Ok(UserLoginCheck { + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + password_hash: v.password_hash.clone(), + }) + }) + .transpose() + } + + pub fn get_login_info_by_handle_or_email( + &self, + identifier: &str, + ) -> Result, MetastoreError> { + self.load_by_identifier(identifier)? + .map(|v| { + Ok(UserLoginInfo { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + email: v.email.clone(), + password_hash: v.password_hash.clone(), + password_required: v.password_required, + two_factor_enabled: v.two_factor_enabled, + preferred_comms_channel: Self::comms_channel(&v), + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: v.takedown_ref.clone(), + channel_verification: Self::channel_verification(&v), + account_type: u8_to_account_type(v.account_type) + .unwrap_or(AccountType::Personal), + }) + }) + .transpose() + } + + pub fn get_2fa_status_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + Ok(self.load_user_by_did(did.as_str())?.map(|v| User2faStatus { + id: v.id, + two_factor_enabled: v.two_factor_enabled, + preferred_comms_channel: Self::comms_channel(&v), + channel_verification: Self::channel_verification(&v), + })) + } + + pub fn get_comms_prefs(&self, user_id: Uuid) -> Result, MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + self.load_user(user_hash)? + .map(|v| { + Ok(UserCommsPrefs { + email: v.email.clone(), + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + preferred_channel: Self::comms_channel(&v), + preferred_locale: v.preferred_locale.clone(), + telegram_chat_id: v.telegram_chat_id, + discord_id: v.discord_id.clone(), + signal_username: v.signal_username.clone(), + }) + }) + .transpose() + } + + pub fn get_id_by_did(&self, did: &Did) -> Result, MetastoreError> { + Ok(self.load_user_by_did(did.as_str())?.map(|v| v.id)) + } + + pub fn get_user_key_by_id(&self, user_id: Uuid) -> Result, MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + Ok(self.load_user(user_hash)?.map(|v| UserKeyInfo { + key_bytes: v.key_bytes.clone(), + encryption_version: Some(v.encryption_version), + })) + } + + pub fn get_id_and_handle_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserIdAndHandle { + id: v.id, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + }) + }) + .transpose() + } + + pub fn get_did_web_info_by_handle( + &self, + handle: &Handle, + ) -> Result, MetastoreError> { + self.load_by_handle(handle.as_str())? + .map(|v| { + Ok(UserDidWebInfo { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + migrated_to_pds: v.migrated_to_pds.clone(), + }) + }) + .transpose() + } + + pub fn get_did_web_overrides( + &self, + user_id: Uuid, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let key = did_web_overrides_key(user_hash); + let val: Option = point_lookup( + &self.users, + key.as_slice(), + DidWebOverridesValue::deserialize, + "corrupt did_web_overrides", + )?; + + Ok(val.map(|v| DidWebOverrides { + verification_methods: v + .verification_methods_json + .and_then(|j| serde_json::from_str(&j).ok()) + .unwrap_or(serde_json::Value::Null), + also_known_as: v.also_known_as.unwrap_or_default(), + })) + } + + pub fn get_handle_by_did(&self, did: &Did) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle")) + }) + .transpose() + } + + pub fn is_account_active_by_did(&self, did: &Did) -> Result, MetastoreError> { + Ok(self.load_user_by_did(did.as_str())?.map(|v| v.is_active())) + } + + pub fn get_user_for_deletion( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserForDeletion { + id: v.id, + password_hash: v.password_hash.clone(), + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + }) + }) + .transpose() + } + + pub fn check_handle_exists( + &self, + handle: &Handle, + exclude_user_id: Uuid, + ) -> Result { + match self.load_by_handle(handle.as_str())? { + Some(v) => Ok(v.id != exclude_user_id), + None => Ok(false), + } + } + + pub fn update_handle(&self, user_id: Uuid, handle: &Handle) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let val = self + .load_user(user_hash)? + .ok_or(MetastoreError::InvalidInput("user not found"))?; + + let old_handle = val.handle.clone(); + let mut updated = val; + updated.handle = handle.as_str().to_owned(); + + let mut batch = self.db.batch(); + batch.remove(&self.users, user_by_handle_key(&old_handle).as_slice()); + batch.insert( + &self.users, + user_by_handle_key(handle.as_str()).as_slice(), + user_hash.raw().to_be_bytes(), + ); + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_user_with_key_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + Ok(self.load_user_by_did(did.as_str())?.map(|v| UserKeyWithId { + id: v.id, + key_bytes: v.key_bytes.clone(), + encryption_version: Some(v.encryption_version), + })) + } + + pub fn is_account_migrated(&self, did: &Did) -> Result { + Ok(self + .load_user_by_did(did.as_str())? + .map_or(false, |v| v.migrated_to_pds.is_some())) + } + + pub fn has_verified_comms_channel(&self, did: &Did) -> Result { + Ok(self + .load_user_by_did(did.as_str())? + .map_or(false, |v| v.channel_verification() != 0)) + } + + pub fn get_id_by_handle(&self, handle: &Handle) -> Result, MetastoreError> { + Ok(self.load_by_handle(handle.as_str())?.map(|v| v.id)) + } + + pub fn get_email_info_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserEmailInfo { + id: v.id, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: v.email.clone(), + email_verified: v.email_verified, + }) + }) + .transpose() + } + + pub fn check_email_exists( + &self, + email: &str, + exclude_user_id: Uuid, + ) -> Result { + match self.load_by_email(email)? { + Some(v) => Ok(v.id != exclude_user_id), + None => Ok(false), + } + } + + pub fn update_email(&self, user_id: Uuid, email: &str) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let val = self + .load_user(user_hash)? + .ok_or(MetastoreError::InvalidInput("user not found"))?; + + let mut batch = self.db.batch(); + + if let Some(old_email) = &val.email { + batch.remove(&self.users, user_by_email_key(old_email).as_slice()); + } + + batch.insert( + &self.users, + user_by_email_key(email).as_slice(), + user_hash.raw().to_be_bytes(), + ); + + let mut updated = val; + updated.email = Some(email.to_owned()); + updated.email_verified = false; + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn set_email_verified(&self, user_id: Uuid, verified: bool) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.email_verified = verified; + })?; + Ok(()) + } + + pub fn check_email_verified_by_identifier( + &self, + identifier: &str, + ) -> Result, MetastoreError> { + Ok(self + .load_by_identifier(identifier)? + .map(|v| v.email_verified)) + } + + pub fn check_channel_verified_by_did( + &self, + did: &Did, + channel: CommsChannel, + ) -> Result, MetastoreError> { + Ok(self.load_user_by_did(did.as_str())?.map(|v| match channel { + CommsChannel::Email => v.email_verified, + CommsChannel::Discord => v.discord_verified, + CommsChannel::Telegram => v.telegram_verified, + CommsChannel::Signal => v.signal_verified, + })) + } + + pub fn admin_update_email(&self, did: &Did, email: &str) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(0), + }; + + let mut batch = self.db.batch(); + + if let Some(old_email) = &val.email { + batch.remove(&self.users, user_by_email_key(old_email).as_slice()); + } + + batch.insert( + &self.users, + user_by_email_key(email).as_slice(), + user_hash.raw().to_be_bytes(), + ); + + let mut updated = val; + updated.email = Some(email.to_owned()); + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(1) + } + + pub fn admin_update_handle(&self, did: &Did, handle: &Handle) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(0), + }; + + let old_handle = val.handle.clone(); + let mut updated = val; + updated.handle = handle.as_str().to_owned(); + + let mut batch = self.db.batch(); + batch.remove(&self.users, user_by_handle_key(&old_handle).as_slice()); + batch.insert( + &self.users, + user_by_handle_key(handle.as_str()).as_slice(), + user_hash.raw().to_be_bytes(), + ); + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(1) + } + + pub fn admin_update_password( + &self, + did: &Did, + password_hash: &str, + ) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + match self.mutate_user(user_hash, |u| { + u.password_hash = Some(password_hash.to_owned()); + })? { + true => Ok(1), + false => Ok(0), + } + } + + pub fn get_notification_prefs( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(NotificationPrefs { + email: v.email.clone().unwrap_or_default(), + preferred_channel: Self::comms_channel(&v), + discord_id: v.discord_id.clone(), + discord_username: v.discord_username.clone(), + discord_verified: v.discord_verified, + telegram_username: v.telegram_username.clone(), + telegram_verified: v.telegram_verified, + telegram_chat_id: v.telegram_chat_id, + signal_username: v.signal_username.clone(), + signal_verified: v.signal_verified, + }) + }) + .transpose() + } + + pub fn get_id_handle_email_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserIdHandleEmail { + id: v.id, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: v.email.clone(), + }) + }) + .transpose() + } + + pub fn update_preferred_comms_channel( + &self, + did: &Did, + channel: CommsChannel, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let channel_u8 = channel_to_u8(channel); + self.mutate_user(user_hash, |u| { + u.preferred_comms_channel = Some(channel_u8); + })?; + Ok(()) + } + + pub fn clear_discord(&self, user_id: Uuid) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(()), + }; + + let mut batch = self.db.batch(); + + if let Some(username) = &val.discord_username { + batch.remove(&self.users, discord_lookup_key(username).as_slice()); + } + + let mut updated = val; + updated.discord_username = None; + updated.discord_id = None; + updated.discord_verified = false; + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn clear_telegram(&self, user_id: Uuid) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(()), + }; + + let mut batch = self.db.batch(); + + if let Some(username) = &val.telegram_username { + batch.remove(&self.users, telegram_lookup_key(username).as_slice()); + } + + let mut updated = val; + updated.telegram_username = None; + updated.telegram_chat_id = None; + updated.telegram_verified = false; + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn clear_signal(&self, user_id: Uuid) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.signal_username = None; + u.signal_verified = false; + })?; + Ok(()) + } + + pub fn set_unverified_signal( + &self, + user_id: Uuid, + signal_username: &str, + ) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.signal_username = Some(signal_username.to_owned()); + u.signal_verified = false; + })?; + Ok(()) + } + + pub fn set_unverified_telegram( + &self, + user_id: Uuid, + telegram_username: &str, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(()), + }; + + let mut batch = self.db.batch(); + + if let Some(old_username) = &val.telegram_username { + batch.remove(&self.users, telegram_lookup_key(old_username).as_slice()); + } + + batch.insert( + &self.users, + telegram_lookup_key(telegram_username).as_slice(), + user_hash.raw().to_be_bytes(), + ); + + let mut updated = val; + updated.telegram_username = Some(telegram_username.to_owned()); + updated.telegram_verified = false; + updated.telegram_chat_id = None; + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn store_telegram_chat_id( + &self, + telegram_username: &str, + chat_id: i64, + handle: Option<&str>, + ) -> Result, MetastoreError> { + let idx_key = telegram_lookup_key(telegram_username); + let raw = match self + .users + .get(idx_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) if raw.len() >= 8 => raw, + _ => match handle { + Some(h) => { + let val = match self.load_by_handle(h)? { + Some(v) => v, + None => return Ok(None), + }; + let user_hash = UserHash::from_did(&val.did); + self.mutate_user(user_hash, |u| { + u.telegram_chat_id = Some(chat_id); + })?; + return Ok(Some(val.id)); + } + None => return Ok(None), + }, + }; + + let hash_raw = u64::from_be_bytes(raw[..8].try_into().unwrap()); + let user_hash = UserHash::from_raw(hash_raw); + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(None), + }; + let uid = val.id; + self.mutate_user(user_hash, |u| { + u.telegram_chat_id = Some(chat_id); + })?; + Ok(Some(uid)) + } + + pub fn get_telegram_chat_id(&self, user_id: Uuid) -> Result, MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + Ok(self.load_user(user_hash)?.and_then(|v| v.telegram_chat_id)) + } + + pub fn set_unverified_discord( + &self, + user_id: Uuid, + discord_username: &str, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(()), + }; + + let mut batch = self.db.batch(); + + if let Some(old_username) = &val.discord_username { + batch.remove(&self.users, discord_lookup_key(old_username).as_slice()); + } + + batch.insert( + &self.users, + discord_lookup_key(discord_username).as_slice(), + user_hash.raw().to_be_bytes(), + ); + + let mut updated = val; + updated.discord_username = Some(discord_username.to_owned()); + updated.discord_id = None; + updated.discord_verified = false; + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn store_discord_user_id( + &self, + discord_username: &str, + discord_id: &str, + handle: Option<&str>, + ) -> Result, MetastoreError> { + let idx_key = discord_lookup_key(discord_username); + let raw = match self + .users + .get(idx_key.as_slice()) + .map_err(MetastoreError::Fjall)? + { + Some(raw) if raw.len() >= 8 => raw, + _ => match handle { + Some(h) => { + let val = match self.load_by_handle(h)? { + Some(v) => v, + None => return Ok(None), + }; + let user_hash = UserHash::from_did(&val.did); + self.mutate_user(user_hash, |u| { + u.discord_id = Some(discord_id.to_owned()); + })?; + return Ok(Some(val.id)); + } + None => return Ok(None), + }, + }; + + let hash_raw = u64::from_be_bytes(raw[..8].try_into().unwrap()); + let user_hash = UserHash::from_raw(hash_raw); + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(None), + }; + let uid = val.id; + self.mutate_user(user_hash, |u| { + u.discord_id = Some(discord_id.to_owned()); + })?; + Ok(Some(uid)) + } + + pub fn get_verification_info( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserVerificationInfo { + id: v.id, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: v.email.clone(), + channel_verification: Self::channel_verification(&v), + }) + }) + .transpose() + } + + pub fn verify_email_channel(&self, user_id: Uuid, email: &str) -> Result { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let val = match self.load_user(user_hash)? { + Some(v) => v, + None => return Ok(false), + }; + + match val.email.as_deref() == Some(email) { + true => { + self.mutate_user(user_hash, |u| { + u.email_verified = true; + })?; + Ok(true) + } + false => Ok(false), + } + } + + pub fn verify_discord_channel( + &self, + user_id: Uuid, + discord_id: &str, + ) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.discord_id = Some(discord_id.to_owned()); + u.discord_verified = true; + })?; + Ok(()) + } + + pub fn verify_telegram_channel( + &self, + user_id: Uuid, + telegram_username: &str, + ) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + match u.telegram_username.as_deref() == Some(telegram_username) { + true => u.telegram_verified = true, + false => {} + } + })?; + Ok(()) + } + + pub fn verify_signal_channel( + &self, + user_id: Uuid, + signal_username: &str, + ) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + match u.signal_username.as_deref() == Some(signal_username) { + true => u.signal_verified = true, + false => {} + } + })?; + Ok(()) + } + + pub fn set_email_verified_flag(&self, user_id: Uuid) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.email_verified = true; + })?; + Ok(()) + } + + pub fn set_discord_verified_flag(&self, user_id: Uuid) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.discord_verified = true; + })?; + Ok(()) + } + + pub fn set_telegram_verified_flag(&self, user_id: Uuid) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.telegram_verified = true; + })?; + Ok(()) + } + + pub fn set_signal_verified_flag(&self, user_id: Uuid) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.signal_verified = true; + })?; + Ok(()) + } + + pub fn has_totp_enabled(&self, did: &Did) -> Result { + Ok(self + .load_user_by_did(did.as_str())? + .map_or(false, |v| v.totp_enabled)) + } + + pub fn has_passkeys(&self, did: &Did) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let prefix = passkey_prefix(user_hash); + match self.users.prefix(prefix.as_slice()).next() { + Some(guard) => { + guard.into_inner().map_err(MetastoreError::Fjall)?; + Ok(true) + } + None => Ok(false), + } + } + + pub fn get_password_hash_by_did(&self, did: &Did) -> Result, MetastoreError> { + Ok(self + .load_user_by_did(did.as_str())? + .and_then(|v| v.password_hash.clone())) + } + + pub fn get_passkeys_for_user(&self, did: &Did) -> Result, MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let prefix = passkey_prefix(user_hash); + + self.users + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let pv = PasskeyValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt passkey value"))?; + acc.push(Self::to_stored_passkey(&pv)?); + Ok::<_, MetastoreError>(acc) + }) + } + + pub fn get_passkey_by_credential_id( + &self, + credential_id: &[u8], + ) -> Result, MetastoreError> { + let idx_key = passkey_by_cred_key(credential_id); + let idx: Option = point_lookup( + &self.users, + idx_key.as_slice(), + PasskeyIndexValue::deserialize, + "corrupt passkey index", + )?; + + match idx { + Some(iv) => { + let pk_key = passkey_key(UserHash::from_raw(iv.user_hash), iv.passkey_id); + let pv: Option = point_lookup( + &self.users, + pk_key.as_slice(), + PasskeyValue::deserialize, + "corrupt passkey value", + )?; + pv.map(|v| Self::to_stored_passkey(&v)).transpose() + } + None => Ok(None), + } + } + + pub fn save_passkey( + &self, + did: &Did, + credential_id: &[u8], + public_key: &[u8], + friendly_name: Option<&str>, + ) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = PasskeyValue { + id, + did: did.to_string(), + credential_id: credential_id.to_vec(), + public_key: public_key.to_vec(), + sign_count: 0, + created_at_ms: now_ms, + last_used_at_ms: None, + friendly_name: friendly_name.map(str::to_owned), + aaguid: None, + transports: None, + }; + + let index = PasskeyIndexValue { + user_hash: user_hash.raw(), + passkey_id: id, + }; + + let mut batch = self.db.batch(); + batch.insert( + &self.users, + passkey_key(user_hash, id).as_slice(), + value.serialize(), + ); + batch.insert( + &self.users, + passkey_by_cred_key(credential_id).as_slice(), + index.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(id) + } + + pub fn update_passkey_counter( + &self, + credential_id: &[u8], + new_counter: i32, + ) -> Result { + let idx_key = passkey_by_cred_key(credential_id); + let idx: Option = point_lookup( + &self.users, + idx_key.as_slice(), + PasskeyIndexValue::deserialize, + "corrupt passkey index", + )?; + + let iv = match idx { + Some(iv) => iv, + None => return Ok(false), + }; + + let pk_key = passkey_key(UserHash::from_raw(iv.user_hash), iv.passkey_id); + let pv: Option = point_lookup( + &self.users, + pk_key.as_slice(), + PasskeyValue::deserialize, + "corrupt passkey value", + )?; + + match pv { + Some(mut val) => { + val.sign_count = new_counter; + val.last_used_at_ms = Some(Utc::now().timestamp_millis()); + self.users + .insert(pk_key.as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + None => Ok(false), + } + } + + pub fn delete_passkey(&self, id: Uuid, did: &Did) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let pk_key = passkey_key(user_hash, id); + + let pv: Option = point_lookup( + &self.users, + pk_key.as_slice(), + PasskeyValue::deserialize, + "corrupt passkey value", + )?; + + match pv { + Some(val) => { + let mut batch = self.db.batch(); + batch.remove(&self.users, pk_key.as_slice()); + batch.remove( + &self.users, + passkey_by_cred_key(&val.credential_id).as_slice(), + ); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(true) + } + None => Ok(false), + } + } + + pub fn update_passkey_name( + &self, + id: Uuid, + did: &Did, + name: &str, + ) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let pk_key = passkey_key(user_hash, id); + + let pv: Option = point_lookup( + &self.users, + pk_key.as_slice(), + PasskeyValue::deserialize, + "corrupt passkey value", + )?; + + match pv { + Some(mut val) => { + val.friendly_name = Some(name.to_owned()); + self.users + .insert(pk_key.as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + None => Ok(false), + } + } + + pub fn save_webauthn_challenge( + &self, + did: &Did, + challenge_type: WebauthnChallengeType, + state_json: &str, + ) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let type_u8 = challenge_type_to_u8(challenge_type); + let id = Uuid::new_v4(); + let now_ms = Utc::now().timestamp_millis(); + + let value = WebauthnChallengeValue { + id, + challenge_type: type_u8, + state_json: state_json.to_owned(), + created_at_ms: now_ms, + }; + + let key = webauthn_challenge_key(user_hash, type_u8); + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall)?; + + Ok(id) + } + + pub fn load_webauthn_challenge( + &self, + did: &Did, + challenge_type: WebauthnChallengeType, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let type_u8 = challenge_type_to_u8(challenge_type); + let key = webauthn_challenge_key(user_hash, type_u8); + + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + WebauthnChallengeValue::deserialize, + "corrupt webauthn challenge", + )?; + + Ok(val.map(|v| v.state_json)) + } + + pub fn delete_webauthn_challenge( + &self, + did: &Did, + challenge_type: WebauthnChallengeType, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let type_u8 = challenge_type_to_u8(challenge_type); + let key = webauthn_challenge_key(user_hash, type_u8); + self.auth + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_totp_record(&self, did: &Did) -> Result, MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let key = totp_key(user_hash); + let val: Option = point_lookup( + &self.users, + key.as_slice(), + TotpValue::deserialize, + "corrupt totp value", + )?; + + Ok(val.map(|v| TotpRecord { + secret_encrypted: v.secret_encrypted, + encryption_version: v.encryption_version, + verified: v.verified, + })) + } + + pub fn get_totp_record_state( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.get_totp_record(did)? + .map(|r| Ok(TotpRecordState::from(r))) + .transpose() + } + + pub fn upsert_totp_secret( + &self, + did: &Did, + secret_encrypted: &[u8], + encryption_version: i32, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let key = totp_key(user_hash); + + let value = TotpValue { + secret_encrypted: secret_encrypted.to_vec(), + encryption_version, + verified: false, + last_used_at_ms: None, + }; + + self.users + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn set_totp_verified(&self, did: &Did) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let key = totp_key(user_hash); + + let mut val = match point_lookup( + &self.users, + key.as_slice(), + TotpValue::deserialize, + "corrupt totp value", + )? { + Some(v) => v, + None => return Ok(()), + }; + + val.verified = true; + + let mut batch = self.db.batch(); + batch.insert(&self.users, key.as_slice(), val.serialize()); + + let mut user = match self.load_user(user_hash)? { + Some(u) => u, + None => { + batch.commit().map_err(MetastoreError::Fjall)?; + return Ok(()); + } + }; + user.totp_enabled = true; + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + user.serialize(), + ); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn update_totp_last_used(&self, did: &Did) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let key = totp_key(user_hash); + let mut val: TotpValue = match point_lookup( + &self.users, + key.as_slice(), + TotpValue::deserialize, + "corrupt totp value", + )? { + Some(v) => v, + None => return Ok(()), + }; + + val.last_used_at_ms = Some(Utc::now().timestamp_millis()); + self.users + .insert(key.as_slice(), val.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn delete_totp(&self, did: &Did) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let key = totp_key(user_hash); + + let mut batch = self.db.batch(); + batch.remove(&self.users, key.as_slice()); + + if let Some(mut user) = self.load_user(user_hash)? { + user.totp_enabled = false; + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + user.serialize(), + ); + } + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_unused_backup_codes( + &self, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let prefix = backup_code_prefix(user_hash); + + self.users + .prefix(prefix.as_slice()) + .try_fold(Vec::new(), |mut acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = BackupCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt backup code"))?; + match val.used { + false => { + acc.push(StoredBackupCode { + id: val.id, + code_hash: val.code_hash, + }); + Ok(acc) + } + true => Ok(acc), + } + }) + } + + pub fn mark_backup_code_used(&self, code_id: Uuid) -> Result { + let prefix = user_primary_prefix(); + let all_user_hashes: Vec = self + .users + .prefix(prefix.as_slice()) + .filter_map(|guard| { + let (key_bytes, _) = guard.into_inner().ok()?; + (key_bytes.len() >= 9).then(|| { + let hash_raw = u64::from_be_bytes(key_bytes[1..9].try_into().unwrap()); + UserHash::from_raw(hash_raw) + }) + }) + .collect(); + + all_user_hashes + .iter() + .try_fold(false, |found, user_hash| match found { + true => Ok(true), + false => { + let key = backup_code_key(*user_hash, code_id); + let val: Option = point_lookup( + &self.users, + key.as_slice(), + BackupCodeValue::deserialize, + "corrupt backup code", + )?; + match val { + Some(mut bc) => { + bc.used = true; + self.users + .insert(key.as_slice(), bc.serialize()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + None => Ok(false), + } + } + }) + } + + pub fn count_unused_backup_codes(&self, did: &Did) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let prefix = backup_code_prefix(user_hash); + + self.users + .prefix(prefix.as_slice()) + .try_fold(0i64, |acc, guard| { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = BackupCodeValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt backup code"))?; + match val.used { + false => Ok(acc.saturating_add(1)), + true => Ok(acc), + } + }) + } + + pub fn delete_backup_codes(&self, did: &Did) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let prefix = backup_code_prefix(user_hash); + + let keys: Vec> = self + .users + .prefix(prefix.as_slice()) + .filter_map(|guard| { + let (key_bytes, _) = guard.into_inner().ok()?; + Some(key_bytes.to_vec()) + }) + .collect(); + + let count = u64::try_from(keys.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + keys.iter().for_each(|key| { + batch.remove(&self.users, key); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + fn insert_backup_codes_batch( + &self, + batch: &mut fjall::OwnedWriteBatch, + user_hash: UserHash, + code_hashes: &[String], + ) { + code_hashes.iter().for_each(|hash| { + let id = Uuid::new_v4(); + let value = BackupCodeValue { + id, + code_hash: hash.clone(), + used: false, + }; + batch.insert( + &self.users, + backup_code_key(user_hash, id).as_slice(), + value.serialize(), + ); + }); + } + + pub fn insert_backup_codes( + &self, + did: &Did, + code_hashes: &[String], + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let mut batch = self.db.batch(); + self.insert_backup_codes_batch(&mut batch, user_hash, code_hashes); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn enable_totp_with_backup_codes( + &self, + did: &Did, + code_hashes: &[String], + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let mut batch = self.db.batch(); + + if let Some(mut user) = self.load_user(user_hash)? { + user.totp_enabled = true; + user.two_factor_enabled = true; + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + user.serialize(), + ); + } + + self.insert_backup_codes_batch(&mut batch, user_hash, code_hashes); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn delete_totp_and_backup_codes(&self, did: &Did) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let mut batch = self.db.batch(); + + batch.remove(&self.users, totp_key(user_hash).as_slice()); + delete_all_by_prefix( + &self.users, + &mut batch, + backup_code_prefix(user_hash).as_slice(), + )?; + + if let Some(mut user) = self.load_user(user_hash)? { + user.totp_enabled = false; + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + user.serialize(), + ); + } + + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn replace_backup_codes( + &self, + did: &Did, + code_hashes: &[String], + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let mut batch = self.db.batch(); + delete_all_by_prefix( + &self.users, + &mut batch, + backup_code_prefix(user_hash).as_slice(), + )?; + self.insert_backup_codes_batch(&mut batch, user_hash, code_hashes); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn get_session_info_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserSessionInfo { + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: v.email.clone(), + is_admin: v.is_admin, + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: v.takedown_ref.clone(), + preferred_locale: v.preferred_locale.clone(), + preferred_comms_channel: Self::comms_channel(&v), + channel_verification: Self::channel_verification(&v), + migrated_to_pds: v.migrated_to_pds.clone(), + migrated_at: v.migrated_at_ms.and_then(DateTime::from_timestamp_millis), + totp_enabled: v.totp_enabled, + email_2fa_enabled: v.email_2fa_enabled, + }) + }) + .transpose() + } + + pub fn get_legacy_login_pref( + &self, + did: &Did, + ) -> Result, MetastoreError> { + Ok(self + .load_user_by_did(did.as_str())? + .map(|v| UserLegacyLoginPref { + allow_legacy_login: v.allow_legacy_login, + has_mfa: v.two_factor_enabled || v.totp_enabled, + })) + } + + pub fn update_legacy_login(&self, did: &Did, allow: bool) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + self.mutate_user(user_hash, |u| { + u.allow_legacy_login = allow; + }) + } + + pub fn update_locale(&self, did: &Did, locale: &str) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + self.mutate_user(user_hash, |u| { + u.preferred_locale = Some(locale.to_owned()); + }) + } + + pub fn get_login_full_by_identifier( + &self, + identifier: &str, + ) -> Result, MetastoreError> { + self.load_by_identifier(identifier)? + .map(|v| { + Ok(UserLoginFull { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + password_hash: v.password_hash.clone(), + email: v.email.clone(), + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + takedown_ref: v.takedown_ref.clone(), + channel_verification: Self::channel_verification(&v), + allow_legacy_login: v.allow_legacy_login, + migrated_to_pds: v.migrated_to_pds.clone(), + preferred_comms_channel: Self::comms_channel(&v), + key_bytes: v.key_bytes.clone(), + encryption_version: Some(v.encryption_version), + totp_enabled: v.totp_enabled, + email_2fa_enabled: v.email_2fa_enabled, + }) + }) + .transpose() + } + + pub fn get_confirm_signup_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserConfirmSignup { + id: v.id, + did: Did::new(v.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: v.email.clone(), + channel: Self::comms_channel(&v), + discord_username: v.discord_username.clone(), + telegram_username: v.telegram_username.clone(), + signal_username: v.signal_username.clone(), + key_bytes: v.key_bytes.clone(), + encryption_version: Some(v.encryption_version), + }) + }) + .transpose() + } + + pub fn get_resend_verification_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserResendVerification { + id: v.id, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + email: v.email.clone(), + channel: Self::comms_channel(&v), + discord_username: v.discord_username.clone(), + telegram_username: v.telegram_username.clone(), + signal_username: v.signal_username.clone(), + channel_verification: Self::channel_verification(&v), + }) + }) + .transpose() + } + + pub fn set_channel_verified( + &self, + did: &Did, + channel: CommsChannel, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + self.mutate_user(user_hash, |u| match channel { + CommsChannel::Email => u.email_verified = true, + CommsChannel::Discord => u.discord_verified = true, + CommsChannel::Telegram => u.telegram_verified = true, + CommsChannel::Signal => u.signal_verified = true, + })?; + Ok(()) + } + + pub fn get_id_by_email_or_handle( + &self, + email: &str, + handle: &str, + ) -> Result, MetastoreError> { + match self.load_by_email(email)? { + Some(v) => Ok(Some(v.id)), + None => Ok(self.load_by_handle(handle)?.map(|v| v.id)), + } + } + + pub fn count_accounts_by_email(&self, email: &str) -> Result { + match self.load_by_email(email)? { + Some(_) => Ok(1), + None => Ok(0), + } + } + + pub fn get_handles_by_email(&self, email: &str) -> Result, MetastoreError> { + match self.load_by_email(email)? { + Some(v) => Handle::new(v.handle.clone()) + .map(|h| vec![h]) + .map_err(|_| MetastoreError::CorruptData("invalid user handle")), + None => Ok(Vec::new()), + } + } + + pub fn set_password_reset_code( + &self, + user_id: Uuid, + code: &str, + expires_at: DateTime, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let user = self + .load_user(user_hash)? + .ok_or(MetastoreError::InvalidInput("user not found"))?; + + let value = ResetCodeValue { + user_hash: user_hash.raw(), + user_id, + preferred_comms_channel: user.preferred_comms_channel, + code: code.to_owned(), + expires_at_ms: expires_at.timestamp_millis(), + }; + + let key = reset_code_key(code); + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_user_by_reset_code( + &self, + code: &str, + ) -> Result, MetastoreError> { + let key = reset_code_key(code); + let val: Option = point_lookup( + &self.auth, + key.as_slice(), + ResetCodeValue::deserialize, + "corrupt reset code", + )?; + + match val { + Some(rc) => { + let now_ms = Utc::now().timestamp_millis(); + match rc.expires_at_ms > now_ms { + true => { + let user_hash = UserHash::from_raw(rc.user_hash); + let user = self.load_user(user_hash)?; + match user { + Some(u) => Ok(Some(UserResetCodeInfo { + id: u.id, + did: Did::new(u.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + preferred_comms_channel: Self::comms_channel(&u), + expires_at: DateTime::from_timestamp_millis(rc.expires_at_ms), + })), + None => Ok(None), + } + } + false => Ok(None), + } + } + None => Ok(None), + } + } + + pub fn clear_password_reset_code(&self, user_id: Uuid) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let user = match self.load_user(user_hash)? { + Some(u) => u, + None => return Ok(()), + }; + let _ = user; + + let prefix = [super::keys::KeyTag::USER_RESET_CODE.raw()]; + let keys_to_remove: Vec> = self + .auth + .prefix(&prefix) + .filter_map(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().ok()?; + let rc = ResetCodeValue::deserialize(&val_bytes)?; + match rc.user_id == user_id { + true => Some(key_bytes.to_vec()), + false => None, + } + }) + .collect(); + + match keys_to_remove.is_empty() { + true => Ok(()), + false => { + let mut batch = self.db.batch(); + keys_to_remove.iter().for_each(|key| { + batch.remove(&self.auth, key); + }); + batch.commit().map_err(MetastoreError::Fjall) + } + } + } + + pub fn get_id_and_password_hash_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .and_then(|v| v.password_hash.clone().map(|ph| (v.id, ph))) + .map(|(id, ph)| { + Ok(UserIdAndPasswordHash { + id, + password_hash: ph, + }) + }) + .transpose() + } + + pub fn update_password_hash( + &self, + user_id: Uuid, + password_hash: &str, + ) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.password_hash = Some(password_hash.to_owned()); + })?; + Ok(()) + } + + pub fn reset_password_with_sessions( + &self, + user_id: Uuid, + password_hash: &str, + ) -> Result { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let user = self + .load_user(user_hash)? + .ok_or(MetastoreError::InvalidInput("user not found"))?; + + let did = Did::new(user.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?; + + let prefix = super::sessions::session_by_did_prefix(user_hash); + let session_jtis: Vec = self + .auth + .prefix(prefix.as_slice()) + .filter_map(|guard| { + let (key_bytes, _) = guard.into_inner().ok()?; + let remaining = key_bytes.get(9..13)?; + let sid = i32::from_be_bytes(remaining.try_into().ok()?); + let session_key = super::sessions::session_primary_key(sid); + self.auth + .get(session_key.as_slice()) + .ok() + .flatten() + .and_then(|raw| super::sessions::SessionTokenValue::deserialize(&raw)) + .map(|s| s.access_jti) + }) + .collect(); + + let mut updated = user; + updated.password_hash = Some(password_hash.to_owned()); + + self.save_user(user_hash, &updated)?; + + Ok(PasswordResetResult { did, session_jtis }) + } + + pub fn activate_account(&self, did: &Did) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + self.mutate_user(user_hash, |u| { + u.deactivated_at_ms = None; + u.delete_after_ms = None; + }) + } + + pub fn deactivate_account( + &self, + did: &Did, + delete_after: Option>, + ) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + let now_ms = Utc::now().timestamp_millis(); + self.mutate_user(user_hash, |u| { + u.deactivated_at_ms = Some(now_ms); + u.delete_after_ms = delete_after.map(|dt| dt.timestamp_millis()); + }) + } + + pub fn has_password_by_did(&self, did: &Did) -> Result, MetastoreError> { + Ok(self + .load_user_by_did(did.as_str())? + .map(|v| v.password_hash.is_some())) + } + + pub fn get_password_info_by_did( + &self, + did: &Did, + ) -> Result, MetastoreError> { + Ok(self + .load_user_by_did(did.as_str())? + .map(|v| UserPasswordInfo { + id: v.id, + password_hash: v.password_hash.clone(), + })) + } + + pub fn remove_user_password(&self, user_id: Uuid) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.password_hash = None; + u.password_required = false; + })?; + Ok(()) + } + + pub fn set_new_user_password( + &self, + user_id: Uuid, + password_hash: &str, + ) -> Result<(), MetastoreError> { + self.mutate_user_by_uuid(user_id, |u| { + u.password_hash = Some(password_hash.to_owned()); + u.password_required = true; + })?; + Ok(()) + } + + pub fn get_user_key_by_did(&self, did: &Did) -> Result, MetastoreError> { + Ok(self.load_user_by_did(did.as_str())?.map(|v| UserKeyInfo { + key_bytes: v.key_bytes.clone(), + encryption_version: Some(v.encryption_version), + })) + } + + fn delete_user_data( + &self, + batch: &mut fjall::OwnedWriteBatch, + user_hash: UserHash, + user: &UserValue, + ) -> Result<(), MetastoreError> { + batch.remove(&self.users, user_primary_key(user_hash).as_slice()); + batch.remove(&self.users, user_by_handle_key(&user.handle).as_slice()); + + if let Some(email) = &user.email { + batch.remove(&self.users, user_by_email_key(email).as_slice()); + } + + if let Some(tg) = &user.telegram_username { + batch.remove(&self.users, telegram_lookup_key(tg).as_slice()); + } + + if let Some(dc) = &user.discord_username { + batch.remove(&self.users, discord_lookup_key(dc).as_slice()); + } + + self.users + .prefix(passkey_prefix(user_hash).as_slice()) + .try_for_each(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + batch.remove(&self.users, key_bytes.as_ref()); + if let Some(pv) = PasskeyValue::deserialize(&val_bytes) { + batch.remove( + &self.users, + passkey_by_cred_key(&pv.credential_id).as_slice(), + ); + } + Ok::<(), MetastoreError>(()) + })?; + + batch.remove(&self.users, totp_key(user_hash).as_slice()); + delete_all_by_prefix(&self.users, batch, backup_code_prefix(user_hash).as_slice())?; + batch.remove(&self.users, recovery_token_key(user_hash).as_slice()); + batch.remove(&self.users, did_web_overrides_key(user_hash).as_slice()); + + self.delete_auth_data_for_user(batch, user_hash)?; + + Ok(()) + } + + fn delete_auth_data_for_user( + &self, + batch: &mut fjall::OwnedWriteBatch, + user_hash: UserHash, + ) -> Result<(), MetastoreError> { + use super::sessions::{ + SessionTokenValue, session_app_password_prefix, session_by_access_key, + session_by_did_prefix, session_by_refresh_key, session_last_reauth_key, + session_primary_key, + }; + + let did_prefix = session_by_did_prefix(user_hash); + self.auth + .prefix(did_prefix.as_slice()) + .try_for_each(|guard| { + let (key_bytes, _) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let mut reader = super::encoding::KeyReader::new(&key_bytes); + let _tag = reader.tag(); + let _hash = reader.u64(); + let sid_bytes: [u8; 4] = reader + .remaining() + .try_into() + .map_err(|_| MetastoreError::CorruptData("session_by_did key truncated"))?; + let sid = i32::from_be_bytes(sid_bytes); + + let primary = session_primary_key(sid); + if let Some(raw) = self + .auth + .get(primary.as_slice()) + .map_err(MetastoreError::Fjall)? + { + if let Some(session) = SessionTokenValue::deserialize(&raw) { + batch.remove( + &self.auth, + session_by_access_key(&session.access_jti).as_slice(), + ); + batch.remove( + &self.auth, + session_by_refresh_key(&session.refresh_jti).as_slice(), + ); + } + batch.remove(&self.auth, primary.as_slice()); + } + + batch.remove(&self.auth, key_bytes.as_ref()); + Ok::<(), MetastoreError>(()) + })?; + + delete_all_by_prefix( + &self.auth, + batch, + session_app_password_prefix(user_hash).as_slice(), + )?; + + batch.remove(&self.auth, session_last_reauth_key(user_hash).as_slice()); + + batch.remove(&self.auth, webauthn_challenge_key(user_hash, 0).as_slice()); + batch.remove(&self.auth, webauthn_challenge_key(user_hash, 1).as_slice()); + + Ok(()) + } + + pub fn delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let user = match self.load_user(user_hash)? { + Some(u) => u, + None => return Ok(()), + }; + + let mut batch = self.db.batch(); + self.delete_user_data(&mut batch, user_hash, &user)?; + self.user_hashes.stage_remove(&mut batch, &user_id); + batch.commit().map_err(MetastoreError::Fjall) + } + + pub fn set_user_takedown( + &self, + did: &Did, + takedown_ref: Option<&str>, + ) -> Result { + let user_hash = self.resolve_hash(did.as_str()); + self.mutate_user(user_hash, |u| { + u.takedown_ref = takedown_ref.map(str::to_owned); + }) + } + + pub fn admin_delete_account_complete( + &self, + user_id: Uuid, + did: &Did, + ) -> Result<(), MetastoreError> { + self.delete_account_complete(user_id, did) + } + + pub fn get_user_for_did_doc(&self, did: &Did) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserForDidDoc { + id: v.id, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + deactivated_at: v + .deactivated_at_ms + .and_then(DateTime::from_timestamp_millis), + }) + }) + .transpose() + } + + pub fn get_user_for_did_doc_build( + &self, + did: &Did, + ) -> Result, MetastoreError> { + self.load_user_by_did(did.as_str())? + .map(|v| { + Ok(UserForDidDocBuild { + id: v.id, + handle: Handle::new(v.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + migrated_to_pds: v.migrated_to_pds.clone(), + }) + }) + .transpose() + } + + pub fn upsert_did_web_overrides( + &self, + user_id: Uuid, + verification_methods: Option, + also_known_as: Option>, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash_from_uuid(user_id)?; + let key = did_web_overrides_key(user_hash); + + let value = DidWebOverridesValue { + verification_methods_json: verification_methods + .map(|v| serde_json::to_string(&v).unwrap_or_default()), + also_known_as, + }; + + self.users + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn update_migrated_to_pds(&self, did: &Did, endpoint: &str) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let now_ms = Utc::now().timestamp_millis(); + self.mutate_user(user_hash, |u| { + u.migrated_to_pds = Some(endpoint.to_owned()); + u.migrated_at_ms = Some(now_ms); + })?; + Ok(()) + } + + pub fn get_user_for_passkey_setup( + &self, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let user = match self.load_user(user_hash)? { + Some(u) => u, + None => return Ok(None), + }; + + let recovery: Option = point_lookup( + &self.users, + recovery_token_key(user_hash).as_slice(), + RecoveryTokenValue::deserialize, + "corrupt recovery token", + )?; + + Ok(Some(UserForPasskeySetup { + id: user.id, + handle: Handle::new(user.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + recovery_token: recovery.as_ref().map(|r| r.token_hash.clone()), + recovery_token_expires_at: recovery + .as_ref() + .and_then(|r| DateTime::from_timestamp_millis(r.expires_at_ms)), + password_required: user.password_required, + })) + } + + pub fn get_user_for_passkey_recovery( + &self, + identifier: &str, + normalized_handle: &str, + ) -> Result, MetastoreError> { + let val = match self.load_by_identifier(identifier)? { + Some(v) => v, + None => match self.load_by_handle(normalized_handle)? { + Some(v) => v, + None => return Ok(None), + }, + }; + + Ok(Some(UserForPasskeyRecovery { + id: val.id, + did: Did::new(val.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + handle: Handle::new(val.handle.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, + password_required: val.password_required, + })) + } + + pub fn set_recovery_token( + &self, + did: &Did, + token_hash: &str, + expires_at: DateTime, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let key = recovery_token_key(user_hash); + + let value = RecoveryTokenValue { + token_hash: token_hash.to_owned(), + expires_at_ms: expires_at.timestamp_millis(), + }; + + self.users + .insert(key.as_slice(), value.serialize()) + .map_err(MetastoreError::Fjall) + } + + pub fn get_user_for_recovery( + &self, + did: &Did, + ) -> Result, MetastoreError> { + let user_hash = self.resolve_hash(did.as_str()); + let user = match self.load_user(user_hash)? { + Some(u) => u, + None => return Ok(None), + }; + + let recovery: Option = point_lookup( + &self.users, + recovery_token_key(user_hash).as_slice(), + RecoveryTokenValue::deserialize, + "corrupt recovery token", + )?; + + Ok(Some(UserForRecovery { + id: user.id, + did: Did::new(user.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + preferred_comms_channel: Self::comms_channel(&user), + recovery_token: recovery.as_ref().map(|r| r.token_hash.clone()), + recovery_token_expires_at: recovery + .as_ref() + .and_then(|r| DateTime::from_timestamp_millis(r.expires_at_ms)), + })) + } + + pub fn get_accounts_scheduled_for_deletion( + &self, + limit: i64, + ) -> Result, MetastoreError> { + let prefix = user_primary_prefix(); + let limit = usize::try_from(limit).unwrap_or(0); + let now_ms = Utc::now().timestamp_millis(); + + self.users + .prefix(prefix.as_slice()) + .map( + |guard| -> Result, MetastoreError> { + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; + let val = UserValue::deserialize(&val_bytes) + .ok_or(MetastoreError::CorruptData("corrupt user value"))?; + match val.delete_after_ms { + Some(delete_at) if delete_at <= now_ms => { + Ok(Some(ScheduledDeletionAccount { + id: val.id, + did: Did::new(val.did.clone()) + .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, + handle: Handle::new(val.handle.clone()).map_err(|_| { + MetastoreError::CorruptData("invalid user handle") + })?, + })) + } + _ => Ok(None), + } + }, + ) + .filter_map(Result::transpose) + .take(limit) + .collect() + } + + pub fn delete_account_with_firehose( + &self, + user_id: Uuid, + did: &Did, + ) -> Result { + self.delete_account_complete(user_id, did)?; + tracing::warn!( + "delete_account_with_firehose: no firehose event emitted (not yet implemented for tranquil-store)" + ); + Ok(0) + } + + fn build_user_value( + &self, + did: &Did, + handle: &Handle, + email: Option<&str>, + password_hash: Option<&str>, + preferred_comms_channel: CommsChannel, + discord_username: Option<&str>, + telegram_username: Option<&str>, + signal_username: Option<&str>, + deactivated_at: Option>, + encrypted_key_bytes: &[u8], + encryption_version: i32, + account_type: AccountType, + password_required: bool, + is_admin: bool, + ) -> UserValue { + let now_ms = Utc::now().timestamp_millis(); + UserValue { + id: Uuid::new_v4(), + did: did.to_string(), + handle: handle.as_str().to_owned(), + email: email.map(str::to_owned), + email_verified: false, + password_hash: password_hash.map(str::to_owned), + created_at_ms: now_ms, + deactivated_at_ms: deactivated_at.map(|dt| dt.timestamp_millis()), + takedown_ref: None, + is_admin, + preferred_comms_channel: Some(channel_to_u8(preferred_comms_channel)), + key_bytes: encrypted_key_bytes.to_vec(), + encryption_version, + account_type: account_type_to_u8(account_type), + password_required, + two_factor_enabled: false, + email_2fa_enabled: false, + totp_enabled: false, + allow_legacy_login: false, + preferred_locale: None, + invites_disabled: false, + migrated_to_pds: None, + migrated_at_ms: None, + discord_username: discord_username.map(str::to_owned), + discord_id: None, + discord_verified: false, + telegram_username: telegram_username.map(str::to_owned), + telegram_chat_id: None, + telegram_verified: false, + signal_username: signal_username.map(str::to_owned), + signal_verified: false, + delete_after_ms: None, + } + } + + fn write_new_account( + &self, + user_value: &UserValue, + commit_cid: &str, + repo_rev: &str, + ) -> Result { + let user_hash = UserHash::from_did(&user_value.did); + let user_id = user_value.id; + + let cid_link = + CidLink::new(commit_cid).map_err(|e| CreateAccountError::Database(e.to_string()))?; + let cid_bytes = cid_link_to_bytes(&cid_link) + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + + let handle_lower = user_value.handle.to_ascii_lowercase(); + + let repo_meta = RepoMetaValue { + repo_root_cid: cid_bytes, + repo_rev: repo_rev.to_string(), + handle: handle_lower.clone(), + status: RepoStatus::Active, + deactivated_at_ms: user_value + .deactivated_at_ms + .and_then(|ms| u64::try_from(ms).ok()), + takedown_ref: None, + did: Some(user_value.did.clone()), + }; + + let mut batch = self.db.batch(); + + self.user_hashes + .stage_insert(&mut batch, user_id, user_hash) + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + user_value.serialize(), + ); + batch.insert( + &self.users, + user_by_handle_key(user_value.handle.as_str()).as_slice(), + user_hash.raw().to_be_bytes(), + ); + + if let Some(email) = &user_value.email { + batch.insert( + &self.users, + user_by_email_key(email).as_slice(), + user_hash.raw().to_be_bytes(), + ); + } + + if let Some(tg) = &user_value.telegram_username { + batch.insert( + &self.users, + telegram_lookup_key(tg).as_slice(), + user_hash.raw().to_be_bytes(), + ); + } + + if let Some(dc) = &user_value.discord_username { + batch.insert( + &self.users, + discord_lookup_key(dc).as_slice(), + user_hash.raw().to_be_bytes(), + ); + } + + batch.insert( + &self.repo_data, + repo_meta_key(user_hash).as_slice(), + repo_meta.serialize(), + ); + batch.insert( + &self.repo_data, + handle_key(&handle_lower).as_slice(), + user_hash.raw().to_be_bytes(), + ); + + match batch.commit() { + Ok(()) => Ok(CreatePasswordAccountResult { + user_id, + is_admin: user_value.is_admin, + }), + Err(e) => { + self.user_hashes.rollback_insert(&user_id, &user_hash); + Err(CreateAccountError::Database(e.to_string())) + } + } + } + + fn check_availability( + &self, + did: &Did, + handle: &Handle, + email: Option<&str>, + ) -> Result<(), CreateAccountError> { + match self.load_user_by_did(did.as_str()) { + Ok(Some(_)) => return Err(CreateAccountError::DidExists), + Err(e) => return Err(CreateAccountError::Database(e.to_string())), + Ok(None) => {} + } + + match self.load_by_handle(handle.as_str()) { + Ok(Some(_)) => return Err(CreateAccountError::HandleTaken), + Err(e) => return Err(CreateAccountError::Database(e.to_string())), + Ok(None) => {} + } + + if let Some(email) = email { + match self.load_by_email(email) { + Ok(Some(_)) => return Err(CreateAccountError::EmailTaken), + Err(e) => return Err(CreateAccountError::Database(e.to_string())), + Ok(None) => {} + } + } + + Ok(()) + } + + pub fn create_password_account( + &self, + input: &CreatePasswordAccountInput, + ) -> Result { + self.check_availability(&input.did, &input.handle, input.email.as_deref())?; + + let is_admin = self + .is_first_account() + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + + let user_value = self.build_user_value( + &input.did, + &input.handle, + input.email.as_deref(), + Some(&input.password_hash), + input.preferred_comms_channel, + input.discord_username.as_deref(), + input.telegram_username.as_deref(), + input.signal_username.as_deref(), + input.deactivated_at, + &input.encrypted_key_bytes, + input.encryption_version, + AccountType::Personal, + true, + is_admin, + ); + + self.write_new_account(&user_value, &input.commit_cid, &input.repo_rev) + } + + pub fn create_delegated_account( + &self, + input: &CreateDelegatedAccountInput, + ) -> Result { + self.check_availability(&input.did, &input.handle, input.email.as_deref())?; + + let user_value = self.build_user_value( + &input.did, + &input.handle, + input.email.as_deref(), + None, + CommsChannel::Email, + None, + None, + None, + None, + &input.encrypted_key_bytes, + input.encryption_version, + AccountType::Delegated, + false, + false, + ); + + let result = self.write_new_account(&user_value, &input.commit_cid, &input.repo_rev)?; + Ok(result.user_id) + } + + pub fn create_passkey_account( + &self, + input: &CreatePasskeyAccountInput, + ) -> Result { + self.check_availability(&input.did, &input.handle, Some(&input.email))?; + + let is_admin = self + .is_first_account() + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + + let user_value = self.build_user_value( + &input.did, + &input.handle, + Some(&input.email), + None, + input.preferred_comms_channel, + input.discord_username.as_deref(), + input.telegram_username.as_deref(), + input.signal_username.as_deref(), + input.deactivated_at, + &input.encrypted_key_bytes, + input.encryption_version, + AccountType::Personal, + false, + is_admin, + ); + + let result = self.write_new_account(&user_value, &input.commit_cid, &input.repo_rev)?; + + let user_hash = UserHash::from_did(input.did.as_str()); + let recovery = RecoveryTokenValue { + token_hash: input.setup_token_hash.clone(), + expires_at_ms: input.setup_expires_at.timestamp_millis(), + }; + self.users + .insert( + recovery_token_key(user_hash).as_slice(), + recovery.serialize(), + ) + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + + Ok(result) + } + + pub fn create_sso_account( + &self, + input: &CreateSsoAccountInput, + ) -> Result { + self.check_availability(&input.did, &input.handle, input.email.as_deref())?; + + let is_admin = self + .is_first_account() + .map_err(|e| CreateAccountError::Database(e.to_string()))?; + + let user_value = self.build_user_value( + &input.did, + &input.handle, + input.email.as_deref(), + None, + input.preferred_comms_channel, + input.discord_username.as_deref(), + input.telegram_username.as_deref(), + input.signal_username.as_deref(), + None, + &input.encrypted_key_bytes, + input.encryption_version, + AccountType::Personal, + false, + is_admin, + ); + + self.write_new_account(&user_value, &input.commit_cid, &input.repo_rev) + } + + pub fn reactivate_migration_account( + &self, + input: &MigrationReactivationInput, + ) -> Result { + let user_hash = UserHash::from_did(input.did.as_str()); + let user = self + .load_user(user_hash) + .map_err(|e| MigrationReactivationError::Database(e.to_string()))? + .ok_or(MigrationReactivationError::NotFound)?; + + match user.deactivated_at_ms { + None => return Err(MigrationReactivationError::NotDeactivated), + Some(_) => {} + } + + let existing_handle = self + .load_by_handle(input.new_handle.as_str()) + .map_err(|e| MigrationReactivationError::Database(e.to_string()))?; + match existing_handle { + Some(other) if other.id != user.id => { + return Err(MigrationReactivationError::HandleTaken); + } + _ => {} + } + + let old_handle = Handle::new(user.handle.clone()) + .map_err(|_| MigrationReactivationError::Database("invalid handle".to_owned()))?; + let user_id = user.id; + + let mut batch = self.db.batch(); + + batch.remove(&self.users, user_by_handle_key(&user.handle).as_slice()); + batch.insert( + &self.users, + user_by_handle_key(input.new_handle.as_str()).as_slice(), + user_hash.raw().to_be_bytes(), + ); + + if let Some(old_email) = &user.email { + batch.remove(&self.users, user_by_email_key(old_email).as_slice()); + } + if let Some(new_email) = &input.new_email { + batch.insert( + &self.users, + user_by_email_key(new_email).as_slice(), + user_hash.raw().to_be_bytes(), + ); + } + + let mut updated = user; + updated.handle = input.new_handle.as_str().to_owned(); + updated.email = input.new_email.clone(); + updated.email_verified = false; + updated.deactivated_at_ms = None; + updated.delete_after_ms = None; + updated.migrated_to_pds = None; + updated.migrated_at_ms = None; + + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + updated.serialize(), + ); + + batch + .commit() + .map_err(|e| MigrationReactivationError::Database(e.to_string()))?; + + Ok(ReactivatedAccountInfo { + user_id, + old_handle, + }) + } + + pub fn check_handle_available_for_new_account( + &self, + handle: &Handle, + ) -> Result { + let existing = self.load_by_handle(handle.as_str())?; + let reserved_key = handle_reservation_key(handle.as_str()); + let reservation = self + .auth + .get(reserved_key.as_slice()) + .map_err(MetastoreError::Fjall)?; + + match (existing, reservation) { + (None, None) => Ok(true), + _ => Ok(false), + } + } + + pub fn reserve_handle( + &self, + handle: &Handle, + reserved_by: &str, + ) -> Result { + let available = self.check_handle_available_for_new_account(handle)?; + match available { + false => Ok(false), + true => { + let now_ms = Utc::now().timestamp_millis(); + let expires_at_ms = now_ms.saturating_add(600_000); + let value = HandleReservationValue { + reserved_by: reserved_by.to_owned(), + created_at_ms: now_ms, + expires_at_ms, + }; + let key = handle_reservation_key(handle.as_str()); + self.auth + .insert(key.as_slice(), value.serialize_with_ttl()) + .map_err(MetastoreError::Fjall)?; + Ok(true) + } + } + } + + pub fn release_handle_reservation(&self, handle: &Handle) -> Result<(), MetastoreError> { + let key = handle_reservation_key(handle.as_str()); + self.auth + .remove(key.as_slice()) + .map_err(MetastoreError::Fjall) + } + + pub fn cleanup_expired_handle_reservations(&self) -> Result { + let prefix = handle_reservation_prefix(); + let now_ms = Utc::now().timestamp_millis(); + + let expired_keys: Vec> = self + .auth + .prefix(prefix.as_slice()) + .filter_map(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().ok()?; + let val = HandleReservationValue::deserialize(&val_bytes)?; + match val.expires_at_ms <= now_ms { + true => Some(key_bytes.to_vec()), + false => None, + } + }) + .collect(); + + let count = u64::try_from(expired_keys.len()).unwrap_or(u64::MAX); + match count { + 0 => Ok(0), + _ => { + let mut batch = self.db.batch(); + expired_keys.iter().for_each(|key| { + batch.remove(&self.auth, key); + }); + batch.commit().map_err(MetastoreError::Fjall)?; + Ok(count) + } + } + } + + pub fn complete_passkey_setup( + &self, + input: &CompletePasskeySetupInput, + ) -> Result<(), MetastoreError> { + let user_hash = self.resolve_hash(input.did.as_str()); + + self.mutate_user(user_hash, |u| { + u.password_hash = Some(input.app_password_hash.clone()); + u.password_required = false; + })?; + + Ok(()) + } + + pub fn recover_passkey_account( + &self, + input: &RecoverPasskeyAccountInput, + ) -> Result { + let user_hash = self.resolve_hash(input.did.as_str()); + + let passkeys: Vec<(Vec, PasskeyValue)> = self + .users + .prefix(passkey_prefix(user_hash).as_slice()) + .filter_map(|guard| { + let (key_bytes, val_bytes) = guard.into_inner().ok()?; + let pv = PasskeyValue::deserialize(&val_bytes)?; + Some((key_bytes.to_vec(), pv)) + }) + .collect(); + + let passkeys_deleted = u64::try_from(passkeys.len()).unwrap_or(u64::MAX); + + let mut batch = self.db.batch(); + + passkeys.iter().for_each(|(key, pv)| { + batch.remove(&self.users, key); + batch.remove( + &self.users, + passkey_by_cred_key(&pv.credential_id).as_slice(), + ); + }); + + batch.remove(&self.users, recovery_token_key(user_hash).as_slice()); + + if let Some(mut user) = self.load_user(user_hash)? { + user.password_hash = Some(input.password_hash.clone()); + user.password_required = true; + batch.insert( + &self.users, + user_primary_key(user_hash).as_slice(), + user.serialize(), + ); + } + + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(RecoverPasskeyAccountResult { passkeys_deleted }) + } +} diff --git a/crates/tranquil-store/src/metastore/users.rs b/crates/tranquil-store/src/metastore/users.rs new file mode 100644 index 0000000..e0e3c95 --- /dev/null +++ b/crates/tranquil-store/src/metastore/users.rs @@ -0,0 +1,829 @@ +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +use super::encoding::KeyBuilder; +use super::keys::{KeyTag, UserHash}; + +const USER_SCHEMA_VERSION: u8 = 1; +const PASSKEY_SCHEMA_VERSION: u8 = 1; +const TOTP_SCHEMA_VERSION: u8 = 1; +const BACKUP_CODE_SCHEMA_VERSION: u8 = 1; +const WEBAUTHN_CHALLENGE_SCHEMA_VERSION: u8 = 1; +const RESET_CODE_SCHEMA_VERSION: u8 = 1; +const RECOVERY_TOKEN_SCHEMA_VERSION: u8 = 1; +const DID_WEB_OVERRIDES_SCHEMA_VERSION: u8 = 1; +const HANDLE_RESERVATION_SCHEMA_VERSION: u8 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserValue { + pub id: uuid::Uuid, + pub did: String, + pub handle: String, + pub email: Option, + pub email_verified: bool, + pub password_hash: Option, + pub created_at_ms: i64, + pub deactivated_at_ms: Option, + pub takedown_ref: Option, + pub is_admin: bool, + pub preferred_comms_channel: Option, + pub key_bytes: Vec, + pub encryption_version: i32, + pub account_type: u8, + pub password_required: bool, + pub two_factor_enabled: bool, + pub email_2fa_enabled: bool, + pub totp_enabled: bool, + pub allow_legacy_login: bool, + pub preferred_locale: Option, + pub invites_disabled: bool, + pub migrated_to_pds: Option, + pub migrated_at_ms: Option, + pub discord_username: Option, + pub discord_id: Option, + pub discord_verified: bool, + pub telegram_username: Option, + pub telegram_chat_id: Option, + pub telegram_verified: bool, + pub signal_username: Option, + pub signal_verified: bool, + pub delete_after_ms: Option, +} + +impl UserValue { + pub fn serialize(&self) -> Vec { + let payload = postcard::to_allocvec(self).expect("UserValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(USER_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + USER_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } + + pub fn is_active(&self) -> bool { + self.deactivated_at_ms.is_none() && self.takedown_ref.is_none() + } + + pub fn channel_verification(&self) -> u8 { + let mut flags = 0u8; + if self.email_verified { + flags |= 1; + } + if self.discord_verified { + flags |= 2; + } + if self.telegram_verified { + flags |= 4; + } + if self.signal_verified { + flags |= 8; + } + flags + } +} + +pub fn account_type_to_u8(t: tranquil_db_traits::AccountType) -> u8 { + match t { + tranquil_db_traits::AccountType::Personal => 0, + tranquil_db_traits::AccountType::Delegated => 1, + } +} + +pub fn u8_to_account_type(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::AccountType::Personal), + 1 => Some(tranquil_db_traits::AccountType::Delegated), + _ => None, + } +} + +pub fn challenge_type_to_u8(t: tranquil_db_traits::WebauthnChallengeType) -> u8 { + match t { + tranquil_db_traits::WebauthnChallengeType::Registration => 0, + tranquil_db_traits::WebauthnChallengeType::Authentication => 1, + } +} + +pub fn u8_to_challenge_type(v: u8) -> Option { + match v { + 0 => Some(tranquil_db_traits::WebauthnChallengeType::Registration), + 1 => Some(tranquil_db_traits::WebauthnChallengeType::Authentication), + _ => None, + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PasskeyValue { + pub id: uuid::Uuid, + pub did: String, + pub credential_id: Vec, + pub public_key: Vec, + pub sign_count: i32, + pub created_at_ms: i64, + pub last_used_at_ms: Option, + pub friendly_name: Option, + pub aaguid: Option>, + pub transports: Option>, +} + +impl PasskeyValue { + pub fn serialize(&self) -> Vec { + let payload = postcard::to_allocvec(self).expect("PasskeyValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(PASSKEY_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + PASSKEY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TotpValue { + pub secret_encrypted: Vec, + pub encryption_version: i32, + pub verified: bool, + pub last_used_at_ms: Option, +} + +impl TotpValue { + pub fn serialize(&self) -> Vec { + let payload = postcard::to_allocvec(self).expect("TotpValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(TOTP_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + TOTP_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BackupCodeValue { + pub id: uuid::Uuid, + pub code_hash: String, + pub used: bool, +} + +impl BackupCodeValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("BackupCodeValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(BACKUP_CODE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + BACKUP_CODE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WebauthnChallengeValue { + pub id: uuid::Uuid, + pub challenge_type: u8, + pub state_json: String, + pub created_at_ms: i64, +} + +impl WebauthnChallengeValue { + pub fn serialize_with_ttl(&self) -> Vec { + let expires_at_ms = self.created_at_ms.saturating_add(300_000); + let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("WebauthnChallengeValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(WEBAUTHN_CHALLENGE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + WEBAUTHN_CHALLENGE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ResetCodeValue { + pub user_hash: u64, + pub user_id: uuid::Uuid, + pub preferred_comms_channel: Option, + pub code: String, + pub expires_at_ms: i64, +} + +impl ResetCodeValue { + pub fn serialize_with_ttl(&self) -> Vec { + let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("ResetCodeValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(RESET_CODE_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + RESET_CODE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RecoveryTokenValue { + pub token_hash: String, + pub expires_at_ms: i64, +} + +impl RecoveryTokenValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("RecoveryTokenValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(RECOVERY_TOKEN_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + RECOVERY_TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DidWebOverridesValue { + pub verification_methods_json: Option, + pub also_known_as: Option>, +} + +impl DidWebOverridesValue { + pub fn serialize(&self) -> Vec { + let payload = + postcard::to_allocvec(self).expect("DidWebOverridesValue serialization cannot fail"); + let mut buf = Vec::with_capacity(1 + payload.len()); + buf.push(DID_WEB_OVERRIDES_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let (&version, payload) = bytes.split_first()?; + match version { + DID_WEB_OVERRIDES_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HandleReservationValue { + pub reserved_by: String, + pub created_at_ms: i64, + pub expires_at_ms: i64, +} + +impl HandleReservationValue { + pub fn serialize_with_ttl(&self) -> Vec { + let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes(); + let payload = + postcard::to_allocvec(self).expect("HandleReservationValue serialization cannot fail"); + let mut buf = Vec::with_capacity(8 + 1 + payload.len()); + buf.extend_from_slice(&ttl_bytes); + buf.push(HANDLE_RESERVATION_SCHEMA_VERSION); + buf.extend_from_slice(&payload); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + let rest = bytes.get(8..)?; + let (&version, payload) = rest.split_first()?; + match version { + HANDLE_RESERVATION_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), + _ => None, + } + } +} + +pub fn user_primary_key(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_PRIMARY) + .u64(user_hash.raw()) + .build() +} + +pub fn user_primary_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new().tag(KeyTag::USER_PRIMARY).build() +} + +pub fn user_by_handle_key(handle: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_BY_HANDLE) + .string(handle) + .build() +} + +pub fn user_by_email_key(email: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_BY_EMAIL) + .string(email) + .build() +} + +pub fn passkey_key(user_hash: UserHash, passkey_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_PASSKEYS) + .u64(user_hash.raw()) + .bytes(passkey_id.as_bytes()) + .build() +} + +pub fn passkey_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_PASSKEYS) + .u64(user_hash.raw()) + .build() +} + +pub fn passkey_by_cred_key(credential_id: &[u8]) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_PASSKEY_BY_CRED) + .bytes(credential_id) + .build() +} + +pub fn totp_key(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_TOTP) + .u64(user_hash.raw()) + .build() +} + +pub fn backup_code_key(user_hash: UserHash, code_id: uuid::Uuid) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_BACKUP_CODES) + .u64(user_hash.raw()) + .bytes(code_id.as_bytes()) + .build() +} + +pub fn backup_code_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_BACKUP_CODES) + .u64(user_hash.raw()) + .build() +} + +pub fn webauthn_challenge_key(user_hash: UserHash, challenge_type: u8) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_WEBAUTHN_CHALLENGE) + .u64(user_hash.raw()) + .raw(&[challenge_type]) + .build() +} + +pub fn reset_code_key(code: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_RESET_CODE) + .string(code) + .build() +} + +pub fn recovery_token_key(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_RECOVERY_TOKEN) + .u64(user_hash.raw()) + .build() +} + +pub fn did_web_overrides_key(user_hash: UserHash) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_DID_WEB_OVERRIDES) + .u64(user_hash.raw()) + .build() +} + +pub fn handle_reservation_key(handle: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_HANDLE_RESERVATION) + .string(handle) + .build() +} + +pub fn handle_reservation_prefix() -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_HANDLE_RESERVATION) + .build() +} + +pub fn telegram_lookup_key(telegram_username: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_TELEGRAM_LOOKUP) + .string(telegram_username) + .build() +} + +pub fn discord_lookup_key(discord_username: &str) -> SmallVec<[u8; 128]> { + KeyBuilder::new() + .tag(KeyTag::USER_DISCORD_LOOKUP) + .string(discord_username) + .build() +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PasskeyIndexValue { + pub user_hash: u64, + pub passkey_id: uuid::Uuid, +} + +impl PasskeyIndexValue { + pub fn serialize(&self) -> Vec { + let mut buf = Vec::with_capacity(24); + buf.extend_from_slice(&self.user_hash.to_be_bytes()); + buf.extend_from_slice(self.passkey_id.as_bytes()); + buf + } + + pub fn deserialize(bytes: &[u8]) -> Option { + (bytes.len() == 24).then(|| { + let user_hash = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + let passkey_id = uuid::Uuid::from_slice(&bytes[8..24]).unwrap(); + Self { + user_hash, + passkey_id, + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn user_value_roundtrip() { + let val = UserValue { + id: uuid::Uuid::new_v4(), + did: "did:plc:test".to_owned(), + handle: "test.example.com".to_owned(), + email: Some("test@example.com".to_owned()), + email_verified: true, + password_hash: Some("hashed".to_owned()), + created_at_ms: 1700000000000, + deactivated_at_ms: None, + takedown_ref: None, + is_admin: false, + preferred_comms_channel: None, + key_bytes: vec![1, 2, 3], + encryption_version: 1, + account_type: 0, + password_required: true, + two_factor_enabled: false, + email_2fa_enabled: false, + totp_enabled: false, + allow_legacy_login: false, + preferred_locale: None, + invites_disabled: false, + migrated_to_pds: None, + migrated_at_ms: None, + discord_username: None, + discord_id: None, + discord_verified: false, + telegram_username: None, + telegram_chat_id: None, + telegram_verified: false, + signal_username: None, + signal_verified: false, + delete_after_ms: None, + }; + let bytes = val.serialize(); + assert_eq!(bytes[0], USER_SCHEMA_VERSION); + let decoded = UserValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn passkey_value_roundtrip() { + let val = PasskeyValue { + id: uuid::Uuid::new_v4(), + did: "did:plc:test".to_owned(), + credential_id: vec![1, 2, 3, 4], + public_key: vec![5, 6, 7, 8], + sign_count: 42, + created_at_ms: 1700000000000, + last_used_at_ms: None, + friendly_name: Some("my key".to_owned()), + aaguid: None, + transports: Some(vec!["usb".to_owned()]), + }; + let bytes = val.serialize(); + assert_eq!(bytes[0], PASSKEY_SCHEMA_VERSION); + let decoded = PasskeyValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn totp_value_roundtrip() { + let val = TotpValue { + secret_encrypted: vec![10, 20, 30], + encryption_version: 1, + verified: true, + last_used_at_ms: Some(1700000000000), + }; + let bytes = val.serialize(); + let decoded = TotpValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn backup_code_value_roundtrip() { + let val = BackupCodeValue { + id: uuid::Uuid::new_v4(), + code_hash: "hash123".to_owned(), + used: false, + }; + let bytes = val.serialize(); + let decoded = BackupCodeValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn webauthn_challenge_value_roundtrip() { + let val = WebauthnChallengeValue { + id: uuid::Uuid::new_v4(), + challenge_type: 0, + state_json: r#"{"challenge":"abc"}"#.to_owned(), + created_at_ms: 1700000000000, + }; + let bytes = val.serialize_with_ttl(); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, 1700000300000); + let decoded = WebauthnChallengeValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn reset_code_value_roundtrip() { + let val = ResetCodeValue { + user_hash: 0xDEAD, + user_id: uuid::Uuid::new_v4(), + preferred_comms_channel: Some(0), + code: "abc123".to_owned(), + expires_at_ms: 1700000600000, + }; + let bytes = val.serialize_with_ttl(); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, 1700000600000); + let decoded = ResetCodeValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn recovery_token_value_roundtrip() { + let val = RecoveryTokenValue { + token_hash: "tokenhash".to_owned(), + expires_at_ms: 1700000600000, + }; + let bytes = val.serialize(); + let decoded = RecoveryTokenValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn did_web_overrides_value_roundtrip() { + let val = DidWebOverridesValue { + verification_methods_json: Some(r#"[{"id":"key-1"}]"#.to_owned()), + also_known_as: Some(vec!["at://user.example.com".to_owned()]), + }; + let bytes = val.serialize(); + let decoded = DidWebOverridesValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn handle_reservation_value_roundtrip() { + let val = HandleReservationValue { + reserved_by: "signup-flow".to_owned(), + created_at_ms: 1700000000000, + expires_at_ms: 1700000600000, + }; + let bytes = val.serialize_with_ttl(); + let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap()); + assert_eq!(ttl, 1700000600000); + let decoded = HandleReservationValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn passkey_index_value_roundtrip() { + let id = uuid::Uuid::new_v4(); + let val = PasskeyIndexValue { + user_hash: 0xCAFE_BABE, + passkey_id: id, + }; + let bytes = val.serialize(); + let decoded = PasskeyIndexValue::deserialize(&bytes).unwrap(); + assert_eq!(val, decoded); + } + + #[test] + fn key_functions_produce_distinct_prefixes() { + let uh = UserHash::from_did("did:plc:test"); + let id = uuid::Uuid::new_v4(); + let keys: Vec> = vec![ + user_primary_key(uh), + user_by_handle_key("test.handle"), + user_by_email_key("test@email.com"), + passkey_key(uh, id), + passkey_by_cred_key(&[1, 2, 3]), + totp_key(uh), + backup_code_key(uh, id), + webauthn_challenge_key(uh, 0), + reset_code_key("code123"), + recovery_token_key(uh), + did_web_overrides_key(uh), + handle_reservation_key("reserved.handle"), + telegram_lookup_key("tg_user"), + discord_lookup_key("dc_user"), + ]; + let tags: Vec = keys.iter().map(|k| k[0]).collect(); + let mut unique_tags = tags.clone(); + unique_tags.sort(); + unique_tags.dedup(); + assert!(unique_tags.len() >= 12); + } + + #[test] + fn passkey_prefix_is_prefix_of_key() { + let uh = UserHash::from_did("did:plc:test"); + let prefix = passkey_prefix(uh); + let key = passkey_key(uh, uuid::Uuid::new_v4()); + assert!(key.starts_with(prefix.as_slice())); + } + + #[test] + fn backup_code_prefix_is_prefix_of_key() { + let uh = UserHash::from_did("did:plc:test"); + let prefix = backup_code_prefix(uh); + let key = backup_code_key(uh, uuid::Uuid::new_v4()); + assert!(key.starts_with(prefix.as_slice())); + } + + #[test] + fn account_type_roundtrip() { + assert_eq!( + u8_to_account_type(account_type_to_u8( + tranquil_db_traits::AccountType::Personal + )), + Some(tranquil_db_traits::AccountType::Personal) + ); + assert_eq!( + u8_to_account_type(account_type_to_u8( + tranquil_db_traits::AccountType::Delegated + )), + Some(tranquil_db_traits::AccountType::Delegated) + ); + assert_eq!(u8_to_account_type(99), None); + } + + #[test] + fn challenge_type_roundtrip() { + use tranquil_db_traits::WebauthnChallengeType; + assert_eq!( + u8_to_challenge_type(challenge_type_to_u8(WebauthnChallengeType::Registration)), + Some(WebauthnChallengeType::Registration) + ); + assert_eq!( + u8_to_challenge_type(challenge_type_to_u8(WebauthnChallengeType::Authentication)), + Some(WebauthnChallengeType::Authentication) + ); + assert_eq!(u8_to_challenge_type(99), None); + } + + #[test] + fn channel_verification_flags() { + let mut user = UserValue { + id: uuid::Uuid::new_v4(), + did: "did:plc:test".to_owned(), + handle: "t.invalid".to_owned(), + email: None, + email_verified: false, + password_hash: None, + created_at_ms: 0, + deactivated_at_ms: None, + takedown_ref: None, + is_admin: false, + preferred_comms_channel: None, + key_bytes: vec![], + encryption_version: 1, + account_type: 0, + password_required: false, + two_factor_enabled: false, + email_2fa_enabled: false, + totp_enabled: false, + allow_legacy_login: false, + preferred_locale: None, + invites_disabled: false, + migrated_to_pds: None, + migrated_at_ms: None, + discord_username: None, + discord_id: None, + discord_verified: false, + telegram_username: None, + telegram_chat_id: None, + telegram_verified: false, + signal_username: None, + signal_verified: false, + delete_after_ms: None, + }; + assert_eq!(user.channel_verification(), 0); + user.email_verified = true; + assert_eq!(user.channel_verification(), 1); + user.discord_verified = true; + assert_eq!(user.channel_verification(), 3); + user.telegram_verified = true; + assert_eq!(user.channel_verification(), 7); + user.signal_verified = true; + assert_eq!(user.channel_verification(), 15); + } + + #[test] + fn deserialize_unknown_version_returns_none() { + let val = UserValue { + id: uuid::Uuid::new_v4(), + did: String::new(), + handle: String::new(), + email: None, + email_verified: false, + password_hash: None, + created_at_ms: 0, + deactivated_at_ms: None, + takedown_ref: None, + is_admin: false, + preferred_comms_channel: None, + key_bytes: vec![], + encryption_version: 0, + account_type: 0, + password_required: false, + two_factor_enabled: false, + email_2fa_enabled: false, + totp_enabled: false, + allow_legacy_login: false, + preferred_locale: None, + invites_disabled: false, + migrated_to_pds: None, + migrated_at_ms: None, + discord_username: None, + discord_id: None, + discord_verified: false, + telegram_username: None, + telegram_chat_id: None, + telegram_verified: false, + signal_username: None, + signal_verified: false, + delete_after_ms: None, + }; + let mut bytes = val.serialize(); + bytes[0] = 99; + assert!(UserValue::deserialize(&bytes).is_none()); + } +}