Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

tranquil-db crates, repository pattern for db access

+10937 -1
+37
Cargo.lock
··· 6375 6375 "sqlx", 6376 6376 "thiserror 2.0.17", 6377 6377 "tokio", 6378 + "tranquil-db-traits", 6378 6379 "urlencoding", 6379 6380 "uuid", 6380 6381 ] ··· 6394 6395 "sha2", 6395 6396 "subtle", 6396 6397 "thiserror 2.0.17", 6398 + ] 6399 + 6400 + [[package]] 6401 + name = "tranquil-db" 6402 + version = "0.1.0" 6403 + dependencies = [ 6404 + "async-trait", 6405 + "chrono", 6406 + "rand 0.8.5", 6407 + "serde", 6408 + "serde_json", 6409 + "sqlx", 6410 + "thiserror 2.0.17", 6411 + "tracing", 6412 + "tranquil-db-traits", 6413 + "tranquil-oauth", 6414 + "tranquil-types", 6415 + "uuid", 6416 + ] 6417 + 6418 + [[package]] 6419 + name = "tranquil-db-traits" 6420 + version = "0.1.0" 6421 + dependencies = [ 6422 + "async-trait", 6423 + "base64 0.22.1", 6424 + "chrono", 6425 + "serde", 6426 + "serde_json", 6427 + "sqlx", 6428 + "thiserror 2.0.17", 6429 + "tranquil-oauth", 6430 + "tranquil-types", 6431 + "uuid", 6397 6432 ] 6398 6433 6399 6434 [[package]] ··· 6503 6538 "tranquil-cache", 6504 6539 "tranquil-comms", 6505 6540 "tranquil-crypto", 6541 + "tranquil-db", 6542 + "tranquil-db-traits", 6506 6543 "tranquil-infra", 6507 6544 "tranquil-oauth", 6508 6545 "tranquil-repo",
+5 -1
Cargo.toml
··· 11 11 "crates/tranquil-auth", 12 12 "crates/tranquil-oauth", 13 13 "crates/tranquil-comms", 14 + "crates/tranquil-db-traits", 15 + "crates/tranquil-db", 14 16 "crates/tranquil-pds", 15 17 ] 16 18 ··· 30 32 tranquil-auth = { path = "crates/tranquil-auth" } 31 33 tranquil-oauth = { path = "crates/tranquil-oauth" } 32 34 tranquil-comms = { path = "crates/tranquil-comms" } 35 + tranquil-db-traits = { path = "crates/tranquil-db-traits" } 36 + tranquil-db = { path = "crates/tranquil-db" } 33 37 34 38 aes-gcm = "0.10" 35 39 backon = "1" ··· 92 96 tracing = "0.1" 93 97 tracing-subscriber = "0.3" 94 98 urlencoding = "2.1" 95 - uuid = { version = "1.19", features = ["v4", "v5", "v7", "fast-rng"] } 99 + uuid = { version = "1.19", features = ["v4", "v5", "v7", "fast-rng", "serde"] } 96 100 webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] } 97 101 webauthn-rs-proto = "0.5" 98 102 zip = { version = "7.0", default-features = false, features = ["deflate"] }
+17
crates/tranquil-db-traits/Cargo.toml
··· 1 + [package] 2 + name = "tranquil-db-traits" 3 + version.workspace = true 4 + edition.workspace = true 5 + license.workspace = true 6 + 7 + [dependencies] 8 + async-trait = { workspace = true } 9 + base64 = { workspace = true } 10 + chrono = { workspace = true } 11 + serde = { workspace = true } 12 + serde_json = { workspace = true } 13 + sqlx = { workspace = true } 14 + thiserror = { workspace = true } 15 + uuid = { workspace = true } 16 + tranquil-oauth = { workspace = true } 17 + tranquil-types = { workspace = true }
+28
crates/tranquil-db-traits/src/backlink.rs
··· 1 + use async_trait::async_trait; 2 + use tranquil_types::{AtUri, Nsid}; 3 + use uuid::Uuid; 4 + 5 + use crate::DbError; 6 + 7 + #[derive(Debug, Clone)] 8 + pub struct Backlink { 9 + pub uri: AtUri, 10 + pub path: String, 11 + pub link_to: String, 12 + } 13 + 14 + #[async_trait] 15 + pub trait BacklinkRepository: Send + Sync { 16 + async fn get_backlink_conflicts( 17 + &self, 18 + repo_id: Uuid, 19 + collection: &Nsid, 20 + backlinks: &[Backlink], 21 + ) -> Result<Vec<AtUri>, DbError>; 22 + 23 + async fn add_backlinks(&self, repo_id: Uuid, backlinks: &[Backlink]) -> Result<(), DbError>; 24 + 25 + async fn remove_backlinks_by_uri(&self, uri: &AtUri) -> Result<(), DbError>; 26 + 27 + async fn remove_backlinks_by_repo(&self, repo_id: Uuid) -> Result<(), DbError>; 28 + }
+109
crates/tranquil-db-traits/src/backup.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use tranquil_types::Did; 4 + use uuid::Uuid; 5 + 6 + use crate::DbError; 7 + 8 + #[derive(Debug, Clone)] 9 + pub struct BackupRow { 10 + pub id: Uuid, 11 + pub repo_rev: String, 12 + pub repo_root_cid: String, 13 + pub block_count: i32, 14 + pub size_bytes: i64, 15 + pub created_at: DateTime<Utc>, 16 + } 17 + 18 + #[derive(Debug, Clone)] 19 + pub struct BackupStorageInfo { 20 + pub storage_key: String, 21 + pub repo_rev: String, 22 + } 23 + 24 + #[derive(Debug, Clone)] 25 + pub struct BackupForDeletion { 26 + pub id: Uuid, 27 + pub storage_key: String, 28 + pub deactivated_at: Option<DateTime<Utc>>, 29 + } 30 + 31 + #[derive(Debug, Clone)] 32 + pub struct OldBackupInfo { 33 + pub id: Uuid, 34 + pub storage_key: String, 35 + } 36 + 37 + #[derive(Debug, Clone)] 38 + pub struct UserBackupInfo { 39 + pub id: Uuid, 40 + pub did: Did, 41 + pub backup_enabled: bool, 42 + pub deactivated_at: Option<DateTime<Utc>>, 43 + pub repo_root_cid: String, 44 + pub repo_rev: Option<String>, 45 + } 46 + 47 + #[derive(Debug, Clone)] 48 + pub struct BlobExportInfo { 49 + pub cid: String, 50 + pub storage_key: String, 51 + pub mime_type: String, 52 + } 53 + 54 + #[async_trait] 55 + pub trait BackupRepository: Send + Sync { 56 + async fn get_user_backup_status( 57 + &self, 58 + did: &Did, 59 + ) -> Result<Option<(Uuid, bool)>, DbError>; 60 + 61 + async fn list_backups_for_user(&self, user_id: Uuid) -> Result<Vec<BackupRow>, DbError>; 62 + 63 + async fn get_backup_storage_info( 64 + &self, 65 + backup_id: Uuid, 66 + did: &Did, 67 + ) -> Result<Option<BackupStorageInfo>, DbError>; 68 + 69 + async fn get_user_for_backup(&self, did: &Did) -> Result<Option<UserBackupInfo>, DbError>; 70 + 71 + async fn insert_backup( 72 + &self, 73 + user_id: Uuid, 74 + storage_key: &str, 75 + repo_root_cid: &str, 76 + repo_rev: &str, 77 + block_count: i32, 78 + size_bytes: i64, 79 + ) -> Result<Uuid, DbError>; 80 + 81 + async fn get_old_backups( 82 + &self, 83 + user_id: Uuid, 84 + retention_offset: i64, 85 + ) -> Result<Vec<OldBackupInfo>, DbError>; 86 + 87 + async fn delete_backup(&self, backup_id: Uuid) -> Result<(), DbError>; 88 + 89 + async fn get_backup_for_deletion( 90 + &self, 91 + backup_id: Uuid, 92 + did: &Did, 93 + ) -> Result<Option<BackupForDeletion>, DbError>; 94 + 95 + async fn get_user_deactivated_status(&self, did: &Did) 96 + -> Result<Option<Option<DateTime<Utc>>>, DbError>; 97 + 98 + async fn update_backup_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError>; 99 + 100 + async fn get_user_id_by_did(&self, did: &Did) -> Result<Option<Uuid>, DbError>; 101 + 102 + async fn get_blobs_for_export(&self, user_id: Uuid) -> Result<Vec<BlobExportInfo>, DbError>; 103 + 104 + async fn get_users_needing_backup( 105 + &self, 106 + backup_interval_secs: i64, 107 + limit: i64, 108 + ) -> Result<Vec<UserBackupInfo>, DbError>; 109 + }
+100
crates/tranquil-db-traits/src/blob.rs
··· 1 + use async_trait::async_trait; 2 + use serde::{Deserialize, Serialize}; 3 + use tranquil_types::{AtUri, CidLink, Did}; 4 + use uuid::Uuid; 5 + 6 + use crate::DbError; 7 + 8 + #[derive(Debug, Clone, Serialize, Deserialize)] 9 + pub struct BlobMetadata { 10 + pub storage_key: String, 11 + pub mime_type: String, 12 + pub size_bytes: i64, 13 + } 14 + 15 + #[derive(Debug, Clone, Serialize, Deserialize)] 16 + pub struct BlobWithTakedown { 17 + pub cid: CidLink, 18 + pub takedown_ref: Option<String>, 19 + } 20 + 21 + #[derive(Debug, Clone, Serialize, Deserialize)] 22 + pub struct BlobForExport { 23 + pub cid: CidLink, 24 + pub storage_key: String, 25 + pub mime_type: String, 26 + } 27 + 28 + #[derive(Debug, Clone, Serialize, Deserialize)] 29 + pub struct MissingBlobInfo { 30 + pub blob_cid: CidLink, 31 + pub record_uri: AtUri, 32 + } 33 + 34 + #[async_trait] 35 + pub trait BlobRepository: Send + Sync { 36 + async fn insert_blob( 37 + &self, 38 + cid: &CidLink, 39 + mime_type: &str, 40 + size_bytes: i64, 41 + created_by_user: Uuid, 42 + storage_key: &str, 43 + ) -> Result<Option<CidLink>, DbError>; 44 + 45 + async fn get_blob_metadata(&self, cid: &CidLink) -> Result<Option<BlobMetadata>, DbError>; 46 + 47 + async fn get_blob_with_takedown( 48 + &self, 49 + cid: &CidLink, 50 + ) -> Result<Option<BlobWithTakedown>, DbError>; 51 + 52 + async fn get_blob_storage_key(&self, cid: &CidLink) -> Result<Option<String>, DbError>; 53 + 54 + async fn list_blobs_by_user( 55 + &self, 56 + user_id: Uuid, 57 + cursor: Option<&str>, 58 + limit: i64, 59 + ) -> Result<Vec<CidLink>, DbError>; 60 + 61 + async fn list_blobs_since_rev( 62 + &self, 63 + did: &Did, 64 + since: &str, 65 + ) -> Result<Vec<CidLink>, DbError>; 66 + 67 + async fn count_blobs_by_user(&self, user_id: Uuid) -> Result<i64, DbError>; 68 + 69 + async fn sum_blob_storage(&self) -> Result<i64, DbError>; 70 + 71 + async fn update_blob_takedown( 72 + &self, 73 + cid: &CidLink, 74 + takedown_ref: Option<&str>, 75 + ) -> Result<bool, DbError>; 76 + 77 + async fn delete_blob_by_cid(&self, cid: &CidLink) -> Result<bool, DbError>; 78 + 79 + async fn delete_blobs_by_user(&self, user_id: Uuid) -> Result<u64, DbError>; 80 + 81 + async fn get_blob_storage_keys_by_user(&self, user_id: Uuid) -> Result<Vec<String>, DbError>; 82 + 83 + async fn insert_record_blobs( 84 + &self, 85 + repo_id: Uuid, 86 + record_uris: &[AtUri], 87 + blob_cids: &[CidLink], 88 + ) -> Result<(), DbError>; 89 + 90 + async fn list_missing_blobs( 91 + &self, 92 + repo_id: Uuid, 93 + cursor: Option<&str>, 94 + limit: i64, 95 + ) -> Result<Vec<MissingBlobInfo>, DbError>; 96 + 97 + async fn count_distinct_record_blobs(&self, repo_id: Uuid) -> Result<i64, DbError>; 98 + 99 + async fn get_blobs_for_export(&self, repo_id: Uuid) -> Result<Vec<BlobForExport>, DbError>; 100 + }
+141
crates/tranquil-db-traits/src/delegation.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use serde::{Deserialize, Serialize}; 4 + use tranquil_types::{Did, Handle}; 5 + use uuid::Uuid; 6 + 7 + use crate::DbError; 8 + 9 + #[derive(Debug, Clone, Serialize, Deserialize)] 10 + pub struct DelegationGrant { 11 + pub id: Uuid, 12 + pub delegated_did: Did, 13 + pub controller_did: Did, 14 + pub granted_scopes: String, 15 + pub granted_at: DateTime<Utc>, 16 + pub granted_by: Did, 17 + pub revoked_at: Option<DateTime<Utc>>, 18 + pub revoked_by: Option<Did>, 19 + } 20 + 21 + #[derive(Debug, Clone, Serialize, Deserialize)] 22 + pub struct DelegatedAccountInfo { 23 + pub did: Did, 24 + pub handle: Handle, 25 + pub granted_scopes: String, 26 + pub granted_at: DateTime<Utc>, 27 + } 28 + 29 + #[derive(Debug, Clone, Serialize, Deserialize)] 30 + pub struct ControllerInfo { 31 + pub did: Did, 32 + pub handle: Handle, 33 + pub granted_scopes: String, 34 + pub granted_at: DateTime<Utc>, 35 + pub is_active: bool, 36 + } 37 + 38 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 39 + pub enum DelegationActionType { 40 + GrantCreated, 41 + GrantRevoked, 42 + ScopesModified, 43 + TokenIssued, 44 + RepoWrite, 45 + BlobUpload, 46 + AccountAction, 47 + } 48 + 49 + #[derive(Debug, Clone, Serialize, Deserialize)] 50 + pub struct AuditLogEntry { 51 + pub id: Uuid, 52 + pub delegated_did: Did, 53 + pub actor_did: Did, 54 + pub controller_did: Option<Did>, 55 + pub action_type: DelegationActionType, 56 + pub action_details: Option<serde_json::Value>, 57 + pub ip_address: Option<String>, 58 + pub user_agent: Option<String>, 59 + pub created_at: DateTime<Utc>, 60 + } 61 + 62 + #[async_trait] 63 + pub trait DelegationRepository: Send + Sync { 64 + async fn is_delegated_account(&self, did: &Did) -> Result<bool, DbError>; 65 + 66 + async fn create_delegation( 67 + &self, 68 + delegated_did: &Did, 69 + controller_did: &Did, 70 + granted_scopes: &str, 71 + granted_by: &Did, 72 + ) -> Result<Uuid, DbError>; 73 + 74 + async fn revoke_delegation( 75 + &self, 76 + delegated_did: &Did, 77 + controller_did: &Did, 78 + revoked_by: &Did, 79 + ) -> Result<bool, DbError>; 80 + 81 + async fn update_delegation_scopes( 82 + &self, 83 + delegated_did: &Did, 84 + controller_did: &Did, 85 + new_scopes: &str, 86 + ) -> Result<bool, DbError>; 87 + 88 + async fn get_delegation( 89 + &self, 90 + delegated_did: &Did, 91 + controller_did: &Did, 92 + ) -> Result<Option<DelegationGrant>, DbError>; 93 + 94 + async fn get_delegations_for_account( 95 + &self, 96 + delegated_did: &Did, 97 + ) -> Result<Vec<ControllerInfo>, DbError>; 98 + 99 + async fn get_accounts_controlled_by( 100 + &self, 101 + controller_did: &Did, 102 + ) -> Result<Vec<DelegatedAccountInfo>, DbError>; 103 + 104 + async fn get_active_controllers_for_account( 105 + &self, 106 + delegated_did: &Did, 107 + ) -> Result<Vec<ControllerInfo>, DbError>; 108 + 109 + async fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, DbError>; 110 + 111 + async fn has_any_controllers(&self, did: &Did) -> Result<bool, DbError>; 112 + 113 + async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError>; 114 + 115 + async fn log_delegation_action( 116 + &self, 117 + delegated_did: &Did, 118 + actor_did: &Did, 119 + controller_did: Option<&Did>, 120 + action_type: DelegationActionType, 121 + action_details: Option<serde_json::Value>, 122 + ip_address: Option<&str>, 123 + user_agent: Option<&str>, 124 + ) -> Result<Uuid, DbError>; 125 + 126 + async fn get_audit_log_for_account( 127 + &self, 128 + delegated_did: &Did, 129 + limit: i64, 130 + offset: i64, 131 + ) -> Result<Vec<AuditLogEntry>, DbError>; 132 + 133 + async fn get_audit_log_by_controller( 134 + &self, 135 + controller_did: &Did, 136 + limit: i64, 137 + offset: i64, 138 + ) -> Result<Vec<AuditLogEntry>, DbError>; 139 + 140 + async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, DbError>; 141 + }
+39
crates/tranquil-db-traits/src/error.rs
··· 1 + use thiserror::Error; 2 + 3 + #[derive(Debug, Error)] 4 + pub enum DbError { 5 + #[error("Database query error: {0}")] 6 + Query(String), 7 + 8 + #[error("Record not found")] 9 + NotFound, 10 + 11 + #[error("Constraint violation: {0}")] 12 + Constraint(String), 13 + 14 + #[error("Connection error: {0}")] 15 + Connection(String), 16 + 17 + #[error("Transaction error: {0}")] 18 + Transaction(String), 19 + 20 + #[error("Serialization error: {0}")] 21 + Serialization(String), 22 + 23 + #[error("Other database error: {0}")] 24 + Other(String), 25 + } 26 + 27 + impl DbError { 28 + pub fn from_query_error(msg: impl Into<String>) -> Self { 29 + DbError::Query(msg.into()) 30 + } 31 + 32 + pub fn from_constraint_error(msg: impl Into<String>) -> Self { 33 + DbError::Constraint(msg.into()) 34 + } 35 + 36 + pub fn from_connection_error(msg: impl Into<String>) -> Self { 37 + DbError::Connection(msg.into()) 38 + } 39 + }
+339
crates/tranquil-db-traits/src/infra.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use serde::{Deserialize, Serialize}; 4 + use tranquil_types::{CidLink, Did, Handle}; 5 + use uuid::Uuid; 6 + 7 + use crate::DbError; 8 + 9 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 10 + pub enum InviteCodeSortOrder { 11 + #[default] 12 + Recent, 13 + Usage, 14 + } 15 + 16 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 17 + #[sqlx(type_name = "comms_channel", rename_all = "snake_case")] 18 + pub enum CommsChannel { 19 + Email, 20 + Discord, 21 + Telegram, 22 + Signal, 23 + } 24 + 25 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] 26 + #[sqlx(type_name = "comms_type", rename_all = "snake_case")] 27 + pub enum CommsType { 28 + Welcome, 29 + EmailVerification, 30 + PasswordReset, 31 + EmailUpdate, 32 + AccountDeletion, 33 + AdminEmail, 34 + PlcOperation, 35 + TwoFactorCode, 36 + PasskeyRecovery, 37 + LegacyLoginAlert, 38 + MigrationVerification, 39 + ChannelVerification, 40 + } 41 + 42 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] 43 + #[sqlx(type_name = "comms_status", rename_all = "snake_case")] 44 + pub enum CommsStatus { 45 + Pending, 46 + Processing, 47 + Sent, 48 + Failed, 49 + } 50 + 51 + #[derive(Debug, Clone, Serialize, Deserialize)] 52 + pub struct QueuedComms { 53 + pub id: Uuid, 54 + pub user_id: Option<Uuid>, 55 + pub channel: CommsChannel, 56 + pub comms_type: CommsType, 57 + pub status: CommsStatus, 58 + pub recipient: String, 59 + pub subject: Option<String>, 60 + pub body: String, 61 + pub metadata: Option<serde_json::Value>, 62 + pub attempts: i32, 63 + pub max_attempts: i32, 64 + pub last_error: Option<String>, 65 + pub created_at: DateTime<Utc>, 66 + pub updated_at: DateTime<Utc>, 67 + pub scheduled_for: DateTime<Utc>, 68 + pub processed_at: Option<DateTime<Utc>>, 69 + } 70 + 71 + #[derive(Debug, Clone, Serialize, Deserialize)] 72 + pub struct InviteCodeInfo { 73 + pub code: String, 74 + pub available_uses: i32, 75 + pub disabled: bool, 76 + pub for_account: Option<Did>, 77 + pub created_at: DateTime<Utc>, 78 + pub created_by: Option<Did>, 79 + } 80 + 81 + #[derive(Debug, Clone, Serialize, Deserialize)] 82 + pub struct InviteCodeUse { 83 + pub code: String, 84 + pub used_by_did: Did, 85 + pub used_by_handle: Option<Handle>, 86 + pub used_at: DateTime<Utc>, 87 + } 88 + 89 + #[derive(Debug, Clone, Serialize, Deserialize)] 90 + pub struct InviteCodeRow { 91 + pub code: String, 92 + pub available_uses: i32, 93 + pub disabled: Option<bool>, 94 + pub created_by_user: Uuid, 95 + pub created_at: DateTime<Utc>, 96 + } 97 + 98 + #[derive(Debug, Clone)] 99 + pub struct ReservedSigningKey { 100 + pub id: Uuid, 101 + pub private_key_bytes: Vec<u8>, 102 + } 103 + 104 + #[derive(Debug, Clone)] 105 + pub struct DeletionRequest { 106 + pub did: Did, 107 + pub expires_at: DateTime<Utc>, 108 + } 109 + 110 + #[async_trait] 111 + pub trait InfraRepository: Send + Sync { 112 + async fn enqueue_comms( 113 + &self, 114 + user_id: Option<Uuid>, 115 + channel: CommsChannel, 116 + comms_type: CommsType, 117 + recipient: &str, 118 + subject: Option<&str>, 119 + body: &str, 120 + metadata: Option<serde_json::Value>, 121 + ) -> Result<Uuid, DbError>; 122 + 123 + async fn fetch_pending_comms( 124 + &self, 125 + now: DateTime<Utc>, 126 + batch_size: i64, 127 + ) -> Result<Vec<QueuedComms>, DbError>; 128 + 129 + async fn mark_comms_sent(&self, id: Uuid) -> Result<(), DbError>; 130 + 131 + async fn mark_comms_failed(&self, id: Uuid, error: &str) -> Result<(), DbError>; 132 + 133 + async fn create_invite_code( 134 + &self, 135 + code: &str, 136 + use_count: i32, 137 + for_account: Option<&Did>, 138 + ) -> Result<bool, DbError>; 139 + 140 + async fn create_invite_codes_batch( 141 + &self, 142 + codes: &[String], 143 + use_count: i32, 144 + created_by_user: Uuid, 145 + for_account: Option<&Did>, 146 + ) -> Result<(), DbError>; 147 + 148 + async fn get_invite_code_available_uses(&self, code: &str) -> Result<Option<i32>, DbError>; 149 + 150 + async fn is_invite_code_valid(&self, code: &str) -> Result<bool, DbError>; 151 + 152 + async fn decrement_invite_code_uses(&self, code: &str) -> Result<(), DbError>; 153 + 154 + async fn record_invite_code_use(&self, code: &str, used_by_user: Uuid) -> Result<(), DbError>; 155 + 156 + async fn get_invite_codes_for_account( 157 + &self, 158 + for_account: &Did, 159 + ) -> Result<Vec<InviteCodeInfo>, DbError>; 160 + 161 + async fn get_invite_code_uses(&self, code: &str) -> Result<Vec<InviteCodeUse>, DbError>; 162 + 163 + async fn disable_invite_codes_by_code(&self, codes: &[String]) -> Result<(), DbError>; 164 + 165 + async fn disable_invite_codes_by_account(&self, accounts: &[Did]) -> Result<(), DbError>; 166 + 167 + async fn list_invite_codes( 168 + &self, 169 + cursor: Option<&str>, 170 + limit: i64, 171 + sort: InviteCodeSortOrder, 172 + ) -> Result<Vec<InviteCodeRow>, DbError>; 173 + 174 + async fn get_user_dids_by_ids(&self, user_ids: &[Uuid]) -> Result<Vec<(Uuid, Did)>, DbError>; 175 + 176 + async fn get_invite_code_uses_batch( 177 + &self, 178 + codes: &[String], 179 + ) -> Result<Vec<InviteCodeUse>, DbError>; 180 + 181 + async fn get_invites_created_by_user( 182 + &self, 183 + user_id: Uuid, 184 + ) -> Result<Vec<InviteCodeInfo>, DbError>; 185 + 186 + async fn get_invite_code_info(&self, code: &str) -> Result<Option<InviteCodeInfo>, DbError>; 187 + 188 + async fn get_invite_codes_by_users( 189 + &self, 190 + user_ids: &[Uuid], 191 + ) -> Result<Vec<(Uuid, InviteCodeInfo)>, DbError>; 192 + 193 + async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result<Option<String>, DbError>; 194 + 195 + async fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), DbError>; 196 + 197 + async fn delete_invite_codes_by_user(&self, user_id: Uuid) -> Result<(), DbError>; 198 + 199 + async fn reserve_signing_key( 200 + &self, 201 + did: Option<&Did>, 202 + public_key_did_key: &str, 203 + private_key_bytes: &[u8], 204 + expires_at: DateTime<Utc>, 205 + ) -> Result<Uuid, DbError>; 206 + 207 + async fn get_reserved_signing_key( 208 + &self, 209 + public_key_did_key: &str, 210 + ) -> Result<Option<ReservedSigningKey>, DbError>; 211 + 212 + async fn mark_signing_key_used(&self, key_id: Uuid) -> Result<(), DbError>; 213 + 214 + async fn create_deletion_request( 215 + &self, 216 + token: &str, 217 + did: &Did, 218 + expires_at: DateTime<Utc>, 219 + ) -> Result<(), DbError>; 220 + 221 + async fn get_deletion_request(&self, token: &str) -> Result<Option<DeletionRequest>, DbError>; 222 + 223 + async fn delete_deletion_request(&self, token: &str) -> Result<(), DbError>; 224 + 225 + async fn delete_deletion_requests_by_did(&self, did: &Did) -> Result<(), DbError>; 226 + 227 + async fn upsert_account_preference( 228 + &self, 229 + user_id: Uuid, 230 + name: &str, 231 + value_json: serde_json::Value, 232 + ) -> Result<(), DbError>; 233 + 234 + async fn insert_account_preference_if_not_exists( 235 + &self, 236 + user_id: Uuid, 237 + name: &str, 238 + value_json: serde_json::Value, 239 + ) -> Result<(), DbError>; 240 + 241 + async fn get_server_config(&self, key: &str) -> Result<Option<String>, DbError>; 242 + 243 + async fn health_check(&self) -> Result<bool, DbError>; 244 + 245 + async fn insert_report( 246 + &self, 247 + id: i64, 248 + reason_type: &str, 249 + reason: Option<&str>, 250 + subject_json: serde_json::Value, 251 + reported_by_did: &Did, 252 + created_at: DateTime<Utc>, 253 + ) -> Result<(), DbError>; 254 + 255 + async fn delete_plc_tokens_for_user(&self, user_id: Uuid) -> Result<(), DbError>; 256 + 257 + async fn insert_plc_token( 258 + &self, 259 + user_id: Uuid, 260 + token: &str, 261 + expires_at: DateTime<Utc>, 262 + ) -> Result<(), DbError>; 263 + 264 + async fn get_plc_token_expiry( 265 + &self, 266 + user_id: Uuid, 267 + token: &str, 268 + ) -> Result<Option<DateTime<Utc>>, DbError>; 269 + 270 + async fn delete_plc_token(&self, user_id: Uuid, token: &str) -> Result<(), DbError>; 271 + 272 + async fn get_account_preferences( 273 + &self, 274 + user_id: Uuid, 275 + ) -> Result<Vec<(String, serde_json::Value)>, DbError>; 276 + 277 + async fn replace_namespace_preferences( 278 + &self, 279 + user_id: Uuid, 280 + namespace: &str, 281 + preferences: Vec<(String, serde_json::Value)>, 282 + ) -> Result<(), DbError>; 283 + 284 + async fn get_notification_history( 285 + &self, 286 + user_id: Uuid, 287 + limit: i64, 288 + ) -> Result<Vec<NotificationHistoryRow>, DbError>; 289 + 290 + async fn get_server_configs( 291 + &self, 292 + keys: &[&str], 293 + ) -> Result<Vec<(String, String)>, DbError>; 294 + 295 + async fn upsert_server_config(&self, key: &str, value: &str) -> Result<(), DbError>; 296 + 297 + async fn delete_server_config(&self, key: &str) -> Result<(), DbError>; 298 + 299 + async fn get_blob_storage_key_by_cid(&self, cid: &CidLink) -> Result<Option<String>, DbError>; 300 + 301 + async fn delete_blob_by_cid(&self, cid: &CidLink) -> Result<(), DbError>; 302 + 303 + async fn get_admin_account_info_by_did( 304 + &self, 305 + did: &Did, 306 + ) -> Result<Option<AdminAccountInfo>, DbError>; 307 + 308 + async fn get_admin_account_infos_by_dids( 309 + &self, 310 + dids: &[Did], 311 + ) -> Result<Vec<AdminAccountInfo>, DbError>; 312 + 313 + async fn get_invite_code_uses_by_users( 314 + &self, 315 + user_ids: &[Uuid], 316 + ) -> Result<Vec<(Uuid, String)>, DbError>; 317 + } 318 + 319 + #[derive(Debug, Clone)] 320 + pub struct NotificationHistoryRow { 321 + pub created_at: DateTime<Utc>, 322 + pub channel: String, 323 + pub comms_type: String, 324 + pub status: String, 325 + pub subject: Option<String>, 326 + pub body: String, 327 + } 328 + 329 + #[derive(Debug, Clone)] 330 + pub struct AdminAccountInfo { 331 + pub id: Uuid, 332 + pub did: Did, 333 + pub handle: Handle, 334 + pub email: Option<String>, 335 + pub created_at: DateTime<Utc>, 336 + pub invites_disabled: bool, 337 + pub email_verified: bool, 338 + pub deactivated_at: Option<DateTime<Utc>>, 339 + }
+58
crates/tranquil-db-traits/src/lib.rs
··· 1 + mod backlink; 2 + mod backup; 3 + mod blob; 4 + mod delegation; 5 + mod error; 6 + mod infra; 7 + mod oauth; 8 + mod repo; 9 + mod session; 10 + mod user; 11 + 12 + pub use backlink::{Backlink, BacklinkRepository}; 13 + pub use backup::{ 14 + BackupForDeletion, BackupRepository, BackupRow, BackupStorageInfo, BlobExportInfo, 15 + OldBackupInfo, UserBackupInfo, 16 + }; 17 + pub use blob::{ 18 + BlobForExport, BlobMetadata, BlobRepository, BlobWithTakedown, MissingBlobInfo, 19 + }; 20 + pub use delegation::{ 21 + AuditLogEntry, ControllerInfo, DelegatedAccountInfo, DelegationActionType, DelegationGrant, 22 + DelegationRepository, 23 + }; 24 + pub use error::DbError; 25 + pub use infra::{ 26 + AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest, InfraRepository, 27 + InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, InviteCodeUse, NotificationHistoryRow, 28 + QueuedComms, ReservedSigningKey, 29 + }; 30 + pub use oauth::{ 31 + DeviceAccountRow, DeviceTrustInfo, OAuthRepository, OAuthSessionListItem, RefreshTokenLookup, 32 + ScopePreference, TrustedDeviceRow, TwoFactorChallenge, 33 + }; 34 + pub use repo::{ 35 + ApplyCommitError, ApplyCommitInput, ApplyCommitResult, BrokenGenesisCommit, CommitEventData, 36 + EventBlocksCids, FullRecordInfo, ImportBlock, ImportRecord, ImportRepoError, RecordDelete, 37 + RecordInfo, RecordUpsert, RecordWithTakedown, RepoAccountInfo, RepoEventNotifier, 38 + RepoEventReceiver, RepoInfo, RepoListItem, RepoRepository, RepoSeqEvent, RepoWithoutRev, 39 + SequencedEvent, UserNeedingRecordBlobsBackfill, UserWithoutBlocks, 40 + }; 41 + pub use session::{ 42 + AppPasswordCreate, AppPasswordRecord, RefreshSessionResult, SessionForRefresh, SessionListItem, 43 + SessionMfaStatus, SessionRefreshData, SessionRepository, SessionToken, SessionTokenCreate, 44 + }; 45 + pub use user::{ 46 + AccountSearchResult, CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput, 47 + CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult, 48 + DidWebOverrides, MigrationReactivationError, MigrationReactivationInput, NotificationPrefs, 49 + OAuthTokenWithUser, PasswordResetResult, ReactivatedAccountInfo, RecoverPasskeyAccountInput, 50 + RecoverPasskeyAccountResult, ScheduledDeletionAccount, StoredBackupCode, StoredPasskey, 51 + TotpRecord, User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, 52 + UserEmailInfo, UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, 53 + UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle, 54 + UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId, 55 + UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo, 56 + UserRepository, UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo, UserStatus, 57 + UserVerificationInfo, UserWithKey, 58 + };
+245
crates/tranquil-db-traits/src/oauth.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use serde::{Deserialize, Serialize}; 4 + use tranquil_oauth::{AuthorizedClientData, DeviceData, RequestData, TokenData}; 5 + use tranquil_types::{AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, TokenId}; 6 + use uuid::Uuid; 7 + 8 + use crate::DbError; 9 + 10 + #[derive(Debug, Clone, Serialize, Deserialize)] 11 + pub struct ScopePreference { 12 + pub scope: String, 13 + pub granted: bool, 14 + } 15 + 16 + #[derive(Debug, Clone)] 17 + pub struct DeviceAccountRow { 18 + pub did: Did, 19 + pub handle: Handle, 20 + pub email: Option<String>, 21 + pub last_used_at: DateTime<Utc>, 22 + } 23 + 24 + #[derive(Debug, Clone)] 25 + pub struct TwoFactorChallenge { 26 + pub id: Uuid, 27 + pub did: Did, 28 + pub request_uri: String, 29 + pub code: String, 30 + pub attempts: i32, 31 + pub created_at: DateTime<Utc>, 32 + pub expires_at: DateTime<Utc>, 33 + } 34 + 35 + #[derive(Debug, Clone)] 36 + pub struct TrustedDeviceRow { 37 + pub id: String, 38 + pub user_agent: Option<String>, 39 + pub friendly_name: Option<String>, 40 + pub trusted_at: Option<DateTime<Utc>>, 41 + pub trusted_until: Option<DateTime<Utc>>, 42 + pub last_seen_at: DateTime<Utc>, 43 + } 44 + 45 + #[derive(Debug, Clone)] 46 + pub struct DeviceTrustInfo { 47 + pub trusted_at: Option<DateTime<Utc>>, 48 + pub trusted_until: Option<DateTime<Utc>>, 49 + } 50 + 51 + #[derive(Debug, Clone)] 52 + pub struct OAuthSessionListItem { 53 + pub id: i32, 54 + pub token_id: TokenId, 55 + pub created_at: DateTime<Utc>, 56 + pub expires_at: DateTime<Utc>, 57 + pub client_id: ClientId, 58 + } 59 + 60 + pub enum RefreshTokenLookup { 61 + Valid { 62 + db_id: i32, 63 + token_data: TokenData, 64 + }, 65 + InGracePeriod { 66 + db_id: i32, 67 + token_data: TokenData, 68 + rotated_at: DateTime<Utc>, 69 + }, 70 + Used { 71 + original_token_id: i32, 72 + }, 73 + Expired { 74 + db_id: i32, 75 + }, 76 + NotFound, 77 + } 78 + 79 + impl RefreshTokenLookup { 80 + pub fn state(&self) -> &'static str { 81 + match self { 82 + Self::Valid { .. } => "valid", 83 + Self::InGracePeriod { .. } => "grace_period", 84 + Self::Used { .. } => "used", 85 + Self::Expired { .. } => "expired", 86 + Self::NotFound => "not_found", 87 + } 88 + } 89 + } 90 + 91 + #[async_trait] 92 + pub trait OAuthRepository: Send + Sync { 93 + async fn create_token(&self, data: &TokenData) -> Result<i32, DbError>; 94 + async fn get_token_by_id(&self, token_id: &TokenId) -> Result<Option<TokenData>, DbError>; 95 + async fn get_token_by_refresh_token( 96 + &self, 97 + refresh_token: &RefreshToken, 98 + ) -> Result<Option<(i32, TokenData)>, DbError>; 99 + async fn get_token_by_previous_refresh_token( 100 + &self, 101 + refresh_token: &RefreshToken, 102 + ) -> Result<Option<(i32, TokenData)>, DbError>; 103 + async fn rotate_token( 104 + &self, 105 + old_db_id: i32, 106 + new_refresh_token: &RefreshToken, 107 + new_expires_at: DateTime<Utc>, 108 + ) -> Result<(), DbError>; 109 + async fn check_refresh_token_used(&self, refresh_token: &RefreshToken) -> Result<Option<i32>, DbError>; 110 + async fn delete_token(&self, token_id: &TokenId) -> Result<(), DbError>; 111 + async fn delete_token_family(&self, db_id: i32) -> Result<(), DbError>; 112 + async fn list_tokens_for_user(&self, did: &Did) -> Result<Vec<TokenData>, DbError>; 113 + async fn count_tokens_for_user(&self, did: &Did) -> Result<i64, DbError>; 114 + async fn delete_oldest_tokens_for_user( 115 + &self, 116 + did: &Did, 117 + keep_count: i64, 118 + ) -> Result<u64, DbError>; 119 + async fn revoke_tokens_for_client(&self, did: &Did, client_id: &ClientId) -> Result<u64, DbError>; 120 + async fn revoke_tokens_for_controller( 121 + &self, 122 + delegated_did: &Did, 123 + controller_did: &Did, 124 + ) -> Result<u64, DbError>; 125 + 126 + async fn create_authorization_request( 127 + &self, 128 + request_id: &RequestId, 129 + data: &RequestData, 130 + ) -> Result<(), DbError>; 131 + async fn get_authorization_request( 132 + &self, 133 + request_id: &RequestId, 134 + ) -> Result<Option<RequestData>, DbError>; 135 + async fn set_authorization_did( 136 + &self, 137 + request_id: &RequestId, 138 + did: &Did, 139 + device_id: Option<&DeviceId>, 140 + ) -> Result<(), DbError>; 141 + async fn update_authorization_request( 142 + &self, 143 + request_id: &RequestId, 144 + did: &Did, 145 + device_id: Option<&DeviceId>, 146 + code: &AuthorizationCode, 147 + ) -> Result<(), DbError>; 148 + async fn consume_authorization_request_by_code( 149 + &self, 150 + code: &AuthorizationCode, 151 + ) -> Result<Option<RequestData>, DbError>; 152 + async fn delete_authorization_request(&self, request_id: &RequestId) -> Result<(), DbError>; 153 + async fn delete_expired_authorization_requests(&self) -> Result<u64, DbError>; 154 + async fn mark_request_authenticated( 155 + &self, 156 + request_id: &RequestId, 157 + did: &Did, 158 + device_id: Option<&DeviceId>, 159 + ) -> Result<(), DbError>; 160 + async fn update_request_scope(&self, request_id: &RequestId, scope: &str) -> Result<(), DbError>; 161 + async fn set_controller_did(&self, request_id: &RequestId, controller_did: &Did) 162 + -> Result<(), DbError>; 163 + async fn set_request_did(&self, request_id: &RequestId, did: &Did) -> Result<(), DbError>; 164 + 165 + async fn create_device(&self, device_id: &DeviceId, data: &DeviceData) -> Result<(), DbError>; 166 + async fn get_device(&self, device_id: &DeviceId) -> Result<Option<DeviceData>, DbError>; 167 + async fn update_device_last_seen(&self, device_id: &DeviceId) -> Result<(), DbError>; 168 + async fn delete_device(&self, device_id: &DeviceId) -> Result<(), DbError>; 169 + async fn upsert_account_device(&self, did: &Did, device_id: &DeviceId) -> Result<(), DbError>; 170 + async fn get_device_accounts(&self, device_id: &DeviceId) -> Result<Vec<DeviceAccountRow>, DbError>; 171 + async fn verify_account_on_device(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError>; 172 + 173 + async fn check_and_record_dpop_jti(&self, jti: &DPoPProofId) -> Result<bool, DbError>; 174 + async fn cleanup_expired_dpop_jtis(&self, max_age_secs: i64) -> Result<u64, DbError>; 175 + 176 + async fn create_2fa_challenge( 177 + &self, 178 + did: &Did, 179 + request_uri: &RequestId, 180 + ) -> Result<TwoFactorChallenge, DbError>; 181 + async fn get_2fa_challenge( 182 + &self, 183 + request_uri: &RequestId, 184 + ) -> Result<Option<TwoFactorChallenge>, DbError>; 185 + async fn increment_2fa_attempts(&self, id: Uuid) -> Result<i32, DbError>; 186 + async fn delete_2fa_challenge(&self, id: Uuid) -> Result<(), DbError>; 187 + async fn delete_2fa_challenge_by_request_uri(&self, request_uri: &RequestId) -> Result<(), DbError>; 188 + async fn cleanup_expired_2fa_challenges(&self) -> Result<u64, DbError>; 189 + async fn check_user_2fa_enabled(&self, did: &Did) -> Result<bool, DbError>; 190 + 191 + async fn get_scope_preferences( 192 + &self, 193 + did: &Did, 194 + client_id: &ClientId, 195 + ) -> Result<Vec<ScopePreference>, DbError>; 196 + async fn upsert_scope_preferences( 197 + &self, 198 + did: &Did, 199 + client_id: &ClientId, 200 + prefs: &[ScopePreference], 201 + ) -> Result<(), DbError>; 202 + async fn delete_scope_preferences(&self, did: &Did, client_id: &ClientId) -> Result<(), DbError>; 203 + 204 + async fn upsert_authorized_client( 205 + &self, 206 + did: &Did, 207 + client_id: &ClientId, 208 + data: &AuthorizedClientData, 209 + ) -> Result<(), DbError>; 210 + async fn get_authorized_client( 211 + &self, 212 + did: &Did, 213 + client_id: &ClientId, 214 + ) -> Result<Option<AuthorizedClientData>, DbError>; 215 + 216 + async fn list_trusted_devices(&self, did: &Did) -> Result<Vec<TrustedDeviceRow>, DbError>; 217 + async fn get_device_trust_info( 218 + &self, 219 + device_id: &DeviceId, 220 + did: &Did, 221 + ) -> Result<Option<DeviceTrustInfo>, DbError>; 222 + async fn device_belongs_to_user(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError>; 223 + async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError>; 224 + async fn update_device_friendly_name( 225 + &self, 226 + device_id: &DeviceId, 227 + friendly_name: Option<&str>, 228 + ) -> Result<(), DbError>; 229 + async fn trust_device( 230 + &self, 231 + device_id: &DeviceId, 232 + trusted_at: DateTime<Utc>, 233 + trusted_until: DateTime<Utc>, 234 + ) -> Result<(), DbError>; 235 + async fn extend_device_trust( 236 + &self, 237 + device_id: &DeviceId, 238 + trusted_until: DateTime<Utc>, 239 + ) -> Result<(), DbError>; 240 + 241 + async fn list_sessions_by_did(&self, did: &Did) -> Result<Vec<OAuthSessionListItem>, DbError>; 242 + async fn delete_session_by_id(&self, session_id: i32, did: &Did) -> Result<u64, DbError>; 243 + async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError>; 244 + async fn delete_sessions_by_did_except(&self, did: &Did, except_token_id: &TokenId) -> Result<u64, DbError>; 245 + }
+388
crates/tranquil-db-traits/src/repo.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use serde::{Deserialize, Serialize}; 4 + use tranquil_types::{AtUri, CidLink, Did, Handle, Nsid, Rkey}; 5 + use uuid::Uuid; 6 + 7 + use crate::DbError; 8 + 9 + #[derive(Debug, Clone, Serialize, Deserialize)] 10 + pub struct RepoAccountInfo { 11 + pub user_id: Uuid, 12 + pub did: Did, 13 + pub deactivated_at: Option<DateTime<Utc>>, 14 + pub takedown_ref: Option<String>, 15 + pub repo_root_cid: Option<CidLink>, 16 + } 17 + 18 + #[derive(Debug, Clone, Serialize, Deserialize)] 19 + pub struct RepoInfo { 20 + pub user_id: Uuid, 21 + pub repo_root_cid: CidLink, 22 + pub repo_rev: Option<String>, 23 + } 24 + 25 + #[derive(Debug, Clone, Serialize, Deserialize)] 26 + pub struct RecordInfo { 27 + pub rkey: Rkey, 28 + pub record_cid: CidLink, 29 + } 30 + 31 + #[derive(Debug, Clone, Serialize, Deserialize)] 32 + pub struct FullRecordInfo { 33 + pub collection: Nsid, 34 + pub rkey: Rkey, 35 + pub record_cid: CidLink, 36 + } 37 + 38 + #[derive(Debug, Clone, Serialize, Deserialize)] 39 + pub struct RecordWithTakedown { 40 + pub id: Uuid, 41 + pub takedown_ref: Option<String>, 42 + } 43 + 44 + #[derive(Debug, Clone, Serialize, Deserialize)] 45 + pub struct RepoWithoutRev { 46 + pub user_id: Uuid, 47 + pub repo_root_cid: CidLink, 48 + } 49 + 50 + #[derive(Debug, Clone)] 51 + pub struct BrokenGenesisCommit { 52 + pub seq: i64, 53 + pub did: Did, 54 + pub commit_cid: Option<CidLink>, 55 + } 56 + 57 + #[derive(Debug, Clone)] 58 + pub struct UserWithoutBlocks { 59 + pub user_id: Uuid, 60 + pub repo_root_cid: CidLink, 61 + pub repo_rev: Option<String>, 62 + } 63 + 64 + #[derive(Debug, Clone)] 65 + pub struct UserNeedingRecordBlobsBackfill { 66 + pub user_id: Uuid, 67 + pub did: Did, 68 + } 69 + 70 + #[derive(Debug, Clone, Serialize, Deserialize)] 71 + pub struct RepoSeqEvent { 72 + pub seq: i64, 73 + } 74 + 75 + #[derive(Debug, Clone, Serialize, Deserialize)] 76 + pub struct SequencedEvent { 77 + pub seq: i64, 78 + pub did: Did, 79 + pub created_at: DateTime<Utc>, 80 + pub event_type: String, 81 + pub commit_cid: Option<CidLink>, 82 + pub prev_cid: Option<CidLink>, 83 + pub prev_data_cid: Option<CidLink>, 84 + pub ops: Option<serde_json::Value>, 85 + pub blobs: Option<Vec<String>>, 86 + pub blocks_cids: Option<Vec<String>>, 87 + pub handle: Option<Handle>, 88 + pub active: Option<bool>, 89 + pub status: Option<String>, 90 + pub rev: Option<String>, 91 + } 92 + 93 + #[derive(Debug, Clone)] 94 + pub struct CommitEventData { 95 + pub did: Did, 96 + pub event_type: String, 97 + pub commit_cid: Option<CidLink>, 98 + pub prev_cid: Option<CidLink>, 99 + pub ops: Option<serde_json::Value>, 100 + pub blobs: Option<Vec<String>>, 101 + pub blocks_cids: Option<Vec<String>>, 102 + pub prev_data_cid: Option<CidLink>, 103 + pub rev: Option<String>, 104 + } 105 + 106 + #[derive(Debug, Clone, Serialize, Deserialize)] 107 + pub struct EventBlocksCids { 108 + pub blocks_cids: Option<Vec<String>>, 109 + pub commit_cid: Option<CidLink>, 110 + } 111 + 112 + #[derive(Debug, Clone, Serialize, Deserialize)] 113 + pub struct RepoListItem { 114 + pub did: Did, 115 + pub deactivated_at: Option<DateTime<Utc>>, 116 + pub takedown_ref: Option<String>, 117 + pub repo_root_cid: CidLink, 118 + pub repo_rev: Option<String>, 119 + } 120 + 121 + #[derive(Debug, Clone)] 122 + pub struct ImportBlock { 123 + pub cid_bytes: Vec<u8>, 124 + pub data: Vec<u8>, 125 + } 126 + 127 + #[derive(Debug, Clone)] 128 + pub struct ImportRecord { 129 + pub collection: Nsid, 130 + pub rkey: Rkey, 131 + pub record_cid: CidLink, 132 + } 133 + 134 + #[derive(Debug, Clone, PartialEq, Eq)] 135 + pub enum ImportRepoError { 136 + RepoNotFound, 137 + ConcurrentModification, 138 + Database(String), 139 + } 140 + 141 + #[derive(Debug, Clone)] 142 + pub struct RecordUpsert { 143 + pub collection: Nsid, 144 + pub rkey: Rkey, 145 + pub cid: CidLink, 146 + } 147 + 148 + #[derive(Debug, Clone)] 149 + pub struct RecordDelete { 150 + pub collection: Nsid, 151 + pub rkey: Rkey, 152 + } 153 + 154 + #[derive(Debug, Clone)] 155 + pub struct ApplyCommitInput { 156 + pub user_id: Uuid, 157 + pub did: Did, 158 + pub expected_root_cid: Option<CidLink>, 159 + pub new_root_cid: CidLink, 160 + pub new_rev: String, 161 + pub new_block_cids: Vec<Vec<u8>>, 162 + pub obsolete_block_cids: Vec<Vec<u8>>, 163 + pub record_upserts: Vec<RecordUpsert>, 164 + pub record_deletes: Vec<RecordDelete>, 165 + pub commit_event: CommitEventData, 166 + } 167 + 168 + #[derive(Debug, Clone)] 169 + pub struct ApplyCommitResult { 170 + pub seq: i64, 171 + pub is_account_active: bool, 172 + } 173 + 174 + #[derive(Debug, Clone, PartialEq, Eq)] 175 + pub enum ApplyCommitError { 176 + RepoNotFound, 177 + ConcurrentModification, 178 + Database(String), 179 + } 180 + 181 + #[async_trait] 182 + pub trait RepoRepository: Send + Sync { 183 + async fn create_repo( 184 + &self, 185 + user_id: Uuid, 186 + repo_root_cid: &CidLink, 187 + repo_rev: &str, 188 + ) -> Result<(), DbError>; 189 + 190 + async fn update_repo_root( 191 + &self, 192 + user_id: Uuid, 193 + repo_root_cid: &CidLink, 194 + repo_rev: &str, 195 + ) -> Result<(), DbError>; 196 + 197 + async fn update_repo_rev(&self, user_id: Uuid, repo_rev: &str) -> Result<(), DbError>; 198 + 199 + async fn delete_repo(&self, user_id: Uuid) -> Result<(), DbError>; 200 + 201 + async fn get_repo_root_for_update(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError>; 202 + 203 + async fn get_repo(&self, user_id: Uuid) -> Result<Option<RepoInfo>, DbError>; 204 + 205 + async fn get_repo_root_by_did(&self, did: &Did) -> Result<Option<CidLink>, DbError>; 206 + 207 + async fn count_repos(&self) -> Result<i64, DbError>; 208 + 209 + async fn get_repos_without_rev(&self) -> Result<Vec<RepoWithoutRev>, DbError>; 210 + 211 + async fn upsert_records( 212 + &self, 213 + repo_id: Uuid, 214 + collections: &[Nsid], 215 + rkeys: &[Rkey], 216 + record_cids: &[CidLink], 217 + repo_rev: &str, 218 + ) -> Result<(), DbError>; 219 + 220 + async fn delete_records( 221 + &self, 222 + repo_id: Uuid, 223 + collections: &[Nsid], 224 + rkeys: &[Rkey], 225 + ) -> Result<(), DbError>; 226 + 227 + async fn delete_all_records(&self, repo_id: Uuid) -> Result<(), DbError>; 228 + 229 + async fn get_record_cid( 230 + &self, 231 + repo_id: Uuid, 232 + collection: &Nsid, 233 + rkey: &Rkey, 234 + ) -> Result<Option<CidLink>, DbError>; 235 + 236 + async fn list_records( 237 + &self, 238 + repo_id: Uuid, 239 + collection: &Nsid, 240 + cursor: Option<&Rkey>, 241 + limit: i64, 242 + reverse: bool, 243 + rkey_start: Option<&Rkey>, 244 + rkey_end: Option<&Rkey>, 245 + ) -> Result<Vec<RecordInfo>, DbError>; 246 + 247 + async fn get_all_records(&self, repo_id: Uuid) -> Result<Vec<FullRecordInfo>, DbError>; 248 + 249 + async fn list_collections(&self, repo_id: Uuid) -> Result<Vec<Nsid>, DbError>; 250 + 251 + async fn count_records(&self, repo_id: Uuid) -> Result<i64, DbError>; 252 + 253 + async fn count_all_records(&self) -> Result<i64, DbError>; 254 + 255 + async fn get_record_by_cid(&self, cid: &CidLink) -> Result<Option<RecordWithTakedown>, DbError>; 256 + 257 + async fn set_record_takedown(&self, cid: &CidLink, takedown_ref: Option<&str>) 258 + -> Result<(), DbError>; 259 + 260 + async fn insert_user_blocks( 261 + &self, 262 + user_id: Uuid, 263 + block_cids: &[Vec<u8>], 264 + repo_rev: &str, 265 + ) -> Result<(), DbError>; 266 + 267 + async fn delete_user_blocks(&self, user_id: Uuid, block_cids: &[Vec<u8>]) 268 + -> Result<(), DbError>; 269 + 270 + async fn get_user_block_cids_since_rev( 271 + &self, 272 + user_id: Uuid, 273 + since_rev: &str, 274 + ) -> Result<Vec<Vec<u8>>, DbError>; 275 + 276 + async fn count_user_blocks(&self, user_id: Uuid) -> Result<i64, DbError>; 277 + 278 + async fn insert_commit_event(&self, data: &CommitEventData) -> Result<i64, DbError>; 279 + 280 + async fn insert_identity_event(&self, did: &Did, handle: Option<&Handle>) -> Result<i64, DbError>; 281 + 282 + async fn insert_account_event( 283 + &self, 284 + did: &Did, 285 + active: bool, 286 + status: Option<&str>, 287 + ) -> Result<i64, DbError>; 288 + 289 + async fn insert_sync_event( 290 + &self, 291 + did: &Did, 292 + commit_cid: &CidLink, 293 + rev: Option<&str>, 294 + ) -> Result<i64, DbError>; 295 + 296 + async fn insert_genesis_commit_event( 297 + &self, 298 + did: &Did, 299 + commit_cid: &CidLink, 300 + mst_root_cid: &CidLink, 301 + rev: &str, 302 + ) -> Result<i64, DbError>; 303 + 304 + async fn update_seq_blocks_cids(&self, seq: i64, blocks_cids: &[String]) 305 + -> Result<(), DbError>; 306 + 307 + async fn delete_sequences_except(&self, did: &Did, keep_seq: i64) -> Result<(), DbError>; 308 + 309 + async fn get_max_seq(&self) -> Result<i64, DbError>; 310 + 311 + async fn get_min_seq_since(&self, since: DateTime<Utc>) -> Result<Option<i64>, DbError>; 312 + 313 + async fn get_account_with_repo(&self, did: &Did) -> Result<Option<RepoAccountInfo>, DbError>; 314 + 315 + async fn get_events_since_seq( 316 + &self, 317 + since_seq: i64, 318 + limit: Option<i64>, 319 + ) -> Result<Vec<SequencedEvent>, DbError>; 320 + 321 + async fn get_events_in_seq_range( 322 + &self, 323 + start_seq: i64, 324 + end_seq: i64, 325 + ) -> Result<Vec<SequencedEvent>, DbError>; 326 + 327 + async fn get_event_by_seq(&self, seq: i64) -> Result<Option<SequencedEvent>, DbError>; 328 + 329 + async fn get_events_since_cursor( 330 + &self, 331 + cursor: i64, 332 + limit: i64, 333 + ) -> Result<Vec<SequencedEvent>, DbError>; 334 + 335 + async fn get_events_since_rev( 336 + &self, 337 + did: &Did, 338 + since_rev: &str, 339 + ) -> Result<Vec<EventBlocksCids>, DbError>; 340 + 341 + async fn list_repos_paginated( 342 + &self, 343 + cursor_did: Option<&Did>, 344 + limit: i64, 345 + ) -> Result<Vec<RepoListItem>, DbError>; 346 + 347 + async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError>; 348 + 349 + async fn notify_update(&self, seq: i64) -> Result<(), DbError>; 350 + 351 + async fn import_repo_data( 352 + &self, 353 + user_id: Uuid, 354 + blocks: &[ImportBlock], 355 + records: &[ImportRecord], 356 + ) -> Result<(), ImportRepoError>; 357 + 358 + async fn apply_commit( 359 + &self, 360 + input: ApplyCommitInput, 361 + ) -> Result<ApplyCommitResult, ApplyCommitError>; 362 + 363 + async fn get_broken_genesis_commits(&self) -> Result<Vec<BrokenGenesisCommit>, DbError>; 364 + 365 + async fn get_users_without_blocks(&self) -> Result<Vec<UserWithoutBlocks>, DbError>; 366 + 367 + async fn get_users_needing_record_blobs_backfill( 368 + &self, 369 + limit: i64, 370 + ) -> Result<Vec<UserNeedingRecordBlobsBackfill>, DbError>; 371 + 372 + async fn insert_record_blobs( 373 + &self, 374 + repo_id: Uuid, 375 + record_uris: &[AtUri], 376 + blob_cids: &[CidLink], 377 + ) -> Result<(), DbError>; 378 + } 379 + 380 + #[async_trait] 381 + pub trait RepoEventNotifier: Send + Sync { 382 + async fn subscribe(&self) -> Result<Box<dyn RepoEventReceiver>, DbError>; 383 + } 384 + 385 + #[async_trait] 386 + pub trait RepoEventReceiver: Send { 387 + async fn recv(&mut self) -> Option<i64>; 388 + }
+203
crates/tranquil-db-traits/src/session.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use tranquil_types::Did; 4 + use uuid::Uuid; 5 + 6 + use crate::DbError; 7 + 8 + #[derive(Debug, Clone)] 9 + pub struct SessionToken { 10 + pub id: i32, 11 + pub did: Did, 12 + pub access_jti: String, 13 + pub refresh_jti: String, 14 + pub access_expires_at: DateTime<Utc>, 15 + pub refresh_expires_at: DateTime<Utc>, 16 + pub legacy_login: bool, 17 + pub mfa_verified: bool, 18 + pub scope: Option<String>, 19 + pub controller_did: Option<Did>, 20 + pub app_password_name: Option<String>, 21 + pub created_at: DateTime<Utc>, 22 + pub updated_at: DateTime<Utc>, 23 + } 24 + 25 + #[derive(Debug, Clone)] 26 + pub struct SessionTokenCreate { 27 + pub did: Did, 28 + pub access_jti: String, 29 + pub refresh_jti: String, 30 + pub access_expires_at: DateTime<Utc>, 31 + pub refresh_expires_at: DateTime<Utc>, 32 + pub legacy_login: bool, 33 + pub mfa_verified: bool, 34 + pub scope: Option<String>, 35 + pub controller_did: Option<Did>, 36 + pub app_password_name: Option<String>, 37 + } 38 + 39 + #[derive(Debug, Clone)] 40 + pub struct SessionForRefresh { 41 + pub id: i32, 42 + pub did: Did, 43 + pub scope: Option<String>, 44 + pub controller_did: Option<Did>, 45 + pub key_bytes: Vec<u8>, 46 + pub encryption_version: i32, 47 + } 48 + 49 + #[derive(Debug, Clone)] 50 + pub struct SessionListItem { 51 + pub id: i32, 52 + pub access_jti: String, 53 + pub created_at: DateTime<Utc>, 54 + pub refresh_expires_at: DateTime<Utc>, 55 + } 56 + 57 + #[derive(Debug, Clone)] 58 + pub struct AppPasswordRecord { 59 + pub id: Uuid, 60 + pub user_id: Uuid, 61 + pub name: String, 62 + pub password_hash: String, 63 + pub created_at: DateTime<Utc>, 64 + pub privileged: bool, 65 + pub scopes: Option<String>, 66 + pub created_by_controller_did: Option<Did>, 67 + } 68 + 69 + #[derive(Debug, Clone)] 70 + pub struct AppPasswordCreate { 71 + pub user_id: Uuid, 72 + pub name: String, 73 + pub password_hash: String, 74 + pub privileged: bool, 75 + pub scopes: Option<String>, 76 + pub created_by_controller_did: Option<Did>, 77 + } 78 + 79 + #[derive(Debug, Clone)] 80 + pub struct SessionMfaStatus { 81 + pub legacy_login: bool, 82 + pub mfa_verified: bool, 83 + pub last_reauth_at: Option<DateTime<Utc>>, 84 + } 85 + 86 + #[derive(Debug, Clone)] 87 + pub enum RefreshSessionResult { 88 + Success, 89 + TokenAlreadyUsed, 90 + ConcurrentRefresh, 91 + } 92 + 93 + #[derive(Debug, Clone)] 94 + pub struct SessionRefreshData { 95 + pub old_refresh_jti: String, 96 + pub session_id: i32, 97 + pub new_access_jti: String, 98 + pub new_refresh_jti: String, 99 + pub new_access_expires_at: DateTime<Utc>, 100 + pub new_refresh_expires_at: DateTime<Utc>, 101 + } 102 + 103 + #[async_trait] 104 + pub trait SessionRepository: Send + Sync { 105 + async fn create_session(&self, data: &SessionTokenCreate) -> Result<i32, DbError>; 106 + 107 + async fn get_session_by_access_jti( 108 + &self, 109 + access_jti: &str, 110 + ) -> Result<Option<SessionToken>, DbError>; 111 + 112 + async fn get_session_for_refresh( 113 + &self, 114 + refresh_jti: &str, 115 + ) -> Result<Option<SessionForRefresh>, DbError>; 116 + 117 + async fn update_session_tokens( 118 + &self, 119 + session_id: i32, 120 + new_access_jti: &str, 121 + new_refresh_jti: &str, 122 + new_access_expires_at: DateTime<Utc>, 123 + new_refresh_expires_at: DateTime<Utc>, 124 + ) -> Result<(), DbError>; 125 + 126 + async fn delete_session_by_access_jti(&self, access_jti: &str) -> Result<u64, DbError>; 127 + 128 + async fn delete_session_by_id(&self, session_id: i32) -> Result<u64, DbError>; 129 + 130 + async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError>; 131 + 132 + async fn delete_sessions_by_did_except_jti( 133 + &self, 134 + did: &Did, 135 + except_jti: &str, 136 + ) -> Result<u64, DbError>; 137 + 138 + async fn list_sessions_by_did(&self, did: &Did) -> Result<Vec<SessionListItem>, DbError>; 139 + 140 + async fn get_session_access_jti_by_id( 141 + &self, 142 + session_id: i32, 143 + did: &Did, 144 + ) -> Result<Option<String>, DbError>; 145 + 146 + async fn delete_sessions_by_app_password( 147 + &self, 148 + did: &Did, 149 + app_password_name: &str, 150 + ) -> Result<u64, DbError>; 151 + 152 + async fn get_session_jtis_by_app_password( 153 + &self, 154 + did: &Did, 155 + app_password_name: &str, 156 + ) -> Result<Vec<String>, DbError>; 157 + 158 + async fn check_refresh_token_used(&self, refresh_jti: &str) -> Result<Option<i32>, DbError>; 159 + 160 + async fn mark_refresh_token_used( 161 + &self, 162 + refresh_jti: &str, 163 + session_id: i32, 164 + ) -> Result<bool, DbError>; 165 + 166 + async fn list_app_passwords(&self, user_id: Uuid) -> Result<Vec<AppPasswordRecord>, DbError>; 167 + 168 + async fn get_app_passwords_for_login( 169 + &self, 170 + user_id: Uuid, 171 + ) -> Result<Vec<AppPasswordRecord>, DbError>; 172 + 173 + async fn get_app_password_by_name( 174 + &self, 175 + user_id: Uuid, 176 + name: &str, 177 + ) -> Result<Option<AppPasswordRecord>, DbError>; 178 + 179 + async fn create_app_password(&self, data: &AppPasswordCreate) -> Result<Uuid, DbError>; 180 + 181 + async fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result<u64, DbError>; 182 + 183 + async fn delete_app_passwords_by_controller( 184 + &self, 185 + did: &Did, 186 + controller_did: &Did, 187 + ) -> Result<u64, DbError>; 188 + 189 + async fn get_last_reauth_at(&self, did: &Did) -> Result<Option<DateTime<Utc>>, DbError>; 190 + 191 + async fn update_last_reauth(&self, did: &Did) -> Result<DateTime<Utc>, DbError>; 192 + 193 + async fn get_session_mfa_status(&self, did: &Did) -> Result<Option<SessionMfaStatus>, DbError>; 194 + 195 + async fn update_mfa_verified(&self, did: &Did) -> Result<(), DbError>; 196 + 197 + async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError>; 198 + 199 + async fn refresh_session_atomic( 200 + &self, 201 + data: &SessionRefreshData, 202 + ) -> Result<RefreshSessionResult, DbError>; 203 + }
+902
crates/tranquil-db-traits/src/user.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use tranquil_types::{Did, Handle}; 4 + use uuid::Uuid; 5 + 6 + use crate::{CommsChannel, DbError}; 7 + 8 + #[derive(Debug, Clone)] 9 + pub struct UserRow { 10 + pub id: Uuid, 11 + pub did: Did, 12 + pub handle: Handle, 13 + pub email: Option<String>, 14 + pub created_at: DateTime<Utc>, 15 + pub deactivated_at: Option<DateTime<Utc>>, 16 + pub takedown_ref: Option<String>, 17 + pub is_admin: bool, 18 + } 19 + 20 + #[derive(Debug, Clone)] 21 + pub struct UserWithKey { 22 + pub id: Uuid, 23 + pub did: Did, 24 + pub handle: Handle, 25 + pub email: Option<String>, 26 + pub deactivated_at: Option<DateTime<Utc>>, 27 + pub takedown_ref: Option<String>, 28 + pub is_admin: bool, 29 + pub key_bytes: Vec<u8>, 30 + pub encryption_version: Option<i32>, 31 + } 32 + 33 + #[derive(Debug, Clone)] 34 + pub struct UserStatus { 35 + pub deactivated_at: Option<DateTime<Utc>>, 36 + pub takedown_ref: Option<String>, 37 + pub is_admin: bool, 38 + } 39 + 40 + #[derive(Debug, Clone)] 41 + pub struct UserEmailInfo { 42 + pub id: Uuid, 43 + pub handle: Handle, 44 + pub email: Option<String>, 45 + pub email_verified: bool, 46 + } 47 + 48 + #[derive(Debug, Clone)] 49 + pub struct UserLoginCheck { 50 + pub did: Did, 51 + pub password_hash: Option<String>, 52 + } 53 + 54 + #[derive(Debug, Clone)] 55 + pub struct UserLoginInfo { 56 + pub id: Uuid, 57 + pub did: Did, 58 + pub email: Option<String>, 59 + pub password_hash: Option<String>, 60 + pub password_required: bool, 61 + pub two_factor_enabled: bool, 62 + pub preferred_comms_channel: CommsChannel, 63 + pub deactivated_at: Option<DateTime<Utc>>, 64 + pub takedown_ref: Option<String>, 65 + pub email_verified: bool, 66 + pub discord_verified: bool, 67 + pub telegram_verified: bool, 68 + pub signal_verified: bool, 69 + pub account_type: String, 70 + } 71 + 72 + #[derive(Debug, Clone)] 73 + pub struct User2faStatus { 74 + pub id: Uuid, 75 + pub two_factor_enabled: bool, 76 + pub preferred_comms_channel: CommsChannel, 77 + pub email_verified: bool, 78 + pub discord_verified: bool, 79 + pub telegram_verified: bool, 80 + pub signal_verified: bool, 81 + } 82 + 83 + #[async_trait] 84 + pub trait UserRepository: Send + Sync { 85 + async fn get_by_did(&self, did: &Did) -> Result<Option<UserRow>, DbError>; 86 + 87 + async fn get_by_handle(&self, handle: &Handle) -> Result<Option<UserRow>, DbError>; 88 + 89 + async fn get_with_key_by_did(&self, did: &Did) -> Result<Option<UserWithKey>, DbError>; 90 + 91 + async fn get_status_by_did(&self, did: &Did) -> Result<Option<UserStatus>, DbError>; 92 + 93 + async fn count_users(&self) -> Result<i64, DbError>; 94 + 95 + async fn get_session_access_expiry( 96 + &self, 97 + did: &Did, 98 + access_jti: &str, 99 + ) -> Result<Option<DateTime<Utc>>, DbError>; 100 + 101 + async fn get_oauth_token_with_user( 102 + &self, 103 + token_id: &str, 104 + ) -> Result<Option<OAuthTokenWithUser>, DbError>; 105 + 106 + async fn get_user_info_by_did(&self, did: &Did) -> Result<Option<UserInfoForAuth>, DbError>; 107 + 108 + async fn get_any_admin_user_id(&self) -> Result<Option<Uuid>, DbError>; 109 + 110 + async fn set_invites_disabled(&self, did: &Did, disabled: bool) -> Result<bool, DbError>; 111 + 112 + async fn search_accounts( 113 + &self, 114 + cursor_did: Option<&Did>, 115 + email_filter: Option<&str>, 116 + handle_filter: Option<&str>, 117 + limit: i64, 118 + ) -> Result<Vec<AccountSearchResult>, DbError>; 119 + 120 + async fn get_auth_info_by_did(&self, did: &Did) -> Result<Option<UserAuthInfo>, DbError>; 121 + 122 + async fn get_by_email(&self, email: &str) -> Result<Option<UserForVerification>, DbError>; 123 + 124 + async fn get_login_check_by_handle_or_email( 125 + &self, 126 + identifier: &str, 127 + ) -> Result<Option<UserLoginCheck>, DbError>; 128 + 129 + async fn get_login_info_by_handle_or_email( 130 + &self, 131 + identifier: &str, 132 + ) -> Result<Option<UserLoginInfo>, DbError>; 133 + 134 + async fn get_2fa_status_by_did(&self, did: &Did) -> Result<Option<User2faStatus>, DbError>; 135 + 136 + async fn get_comms_prefs(&self, user_id: Uuid) -> Result<Option<UserCommsPrefs>, DbError>; 137 + 138 + async fn get_id_by_did(&self, did: &Did) -> Result<Option<Uuid>, DbError>; 139 + 140 + async fn get_user_key_by_id(&self, user_id: Uuid) -> Result<Option<UserKeyInfo>, DbError>; 141 + 142 + async fn get_id_and_handle_by_did(&self, did: &Did) -> Result<Option<UserIdAndHandle>, DbError>; 143 + 144 + async fn get_did_web_info_by_handle( 145 + &self, 146 + handle: &Handle, 147 + ) -> Result<Option<UserDidWebInfo>, DbError>; 148 + 149 + async fn get_did_web_overrides(&self, user_id: Uuid) -> Result<Option<DidWebOverrides>, DbError>; 150 + 151 + async fn get_handle_by_did(&self, did: &Did) -> Result<Option<Handle>, DbError>; 152 + 153 + async fn is_account_active_by_did(&self, did: &Did) -> Result<Option<bool>, DbError>; 154 + 155 + async fn get_user_for_deletion( 156 + &self, 157 + did: &Did, 158 + ) -> Result<Option<UserForDeletion>, DbError>; 159 + 160 + async fn check_handle_exists(&self, handle: &Handle, exclude_user_id: Uuid) -> Result<bool, DbError>; 161 + 162 + async fn update_handle(&self, user_id: Uuid, handle: &Handle) -> Result<(), DbError>; 163 + 164 + async fn get_user_with_key_by_did( 165 + &self, 166 + did: &Did, 167 + ) -> Result<Option<UserKeyWithId>, DbError>; 168 + 169 + async fn is_account_migrated(&self, did: &Did) -> Result<bool, DbError>; 170 + 171 + async fn has_verified_comms_channel(&self, did: &Did) -> Result<bool, DbError>; 172 + 173 + async fn get_id_by_handle(&self, handle: &Handle) -> Result<Option<Uuid>, DbError>; 174 + 175 + async fn get_email_info_by_did(&self, did: &Did) -> Result<Option<UserEmailInfo>, DbError>; 176 + 177 + async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) -> Result<bool, DbError>; 178 + 179 + async fn update_email(&self, user_id: Uuid, email: &str) -> Result<(), DbError>; 180 + 181 + async fn set_email_verified(&self, user_id: Uuid, verified: bool) -> Result<(), DbError>; 182 + 183 + async fn check_email_verified_by_identifier( 184 + &self, 185 + identifier: &str, 186 + ) -> Result<Option<bool>, DbError>; 187 + 188 + async fn admin_update_email(&self, did: &Did, email: &str) -> Result<u64, DbError>; 189 + 190 + async fn admin_update_handle(&self, did: &Did, handle: &Handle) -> Result<u64, DbError>; 191 + 192 + async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError>; 193 + 194 + async fn get_notification_prefs(&self, did: &Did) -> Result<Option<NotificationPrefs>, DbError>; 195 + 196 + async fn get_id_handle_email_by_did( 197 + &self, 198 + did: &Did, 199 + ) -> Result<Option<UserIdHandleEmail>, DbError>; 200 + 201 + async fn update_preferred_comms_channel(&self, did: &Did, channel: &str) -> Result<(), DbError>; 202 + 203 + async fn clear_discord(&self, user_id: Uuid) -> Result<(), DbError>; 204 + 205 + async fn clear_telegram(&self, user_id: Uuid) -> Result<(), DbError>; 206 + 207 + async fn clear_signal(&self, user_id: Uuid) -> Result<(), DbError>; 208 + 209 + async fn get_verification_info( 210 + &self, 211 + did: &Did, 212 + ) -> Result<Option<UserVerificationInfo>, DbError>; 213 + 214 + async fn verify_email_channel(&self, user_id: Uuid, email: &str) -> Result<bool, DbError>; 215 + 216 + async fn verify_discord_channel(&self, user_id: Uuid, discord_id: &str) -> Result<(), DbError>; 217 + 218 + async fn verify_telegram_channel( 219 + &self, 220 + user_id: Uuid, 221 + telegram_username: &str, 222 + ) -> Result<(), DbError>; 223 + 224 + async fn verify_signal_channel(&self, user_id: Uuid, signal_number: &str) 225 + -> Result<(), DbError>; 226 + 227 + async fn set_email_verified_flag(&self, user_id: Uuid) -> Result<(), DbError>; 228 + 229 + async fn set_discord_verified_flag(&self, user_id: Uuid) -> Result<(), DbError>; 230 + 231 + async fn set_telegram_verified_flag(&self, user_id: Uuid) -> Result<(), DbError>; 232 + 233 + async fn set_signal_verified_flag(&self, user_id: Uuid) -> Result<(), DbError>; 234 + 235 + async fn has_totp_enabled(&self, did: &Did) -> Result<bool, DbError>; 236 + 237 + async fn has_passkeys(&self, did: &Did) -> Result<bool, DbError>; 238 + 239 + async fn get_password_hash_by_did(&self, did: &Did) -> Result<Option<String>, DbError>; 240 + 241 + async fn get_passkeys_for_user(&self, did: &Did) -> Result<Vec<StoredPasskey>, DbError>; 242 + 243 + async fn get_passkey_by_credential_id( 244 + &self, 245 + credential_id: &[u8], 246 + ) -> Result<Option<StoredPasskey>, DbError>; 247 + 248 + async fn save_passkey( 249 + &self, 250 + did: &Did, 251 + credential_id: &[u8], 252 + public_key: &[u8], 253 + friendly_name: Option<&str>, 254 + ) -> Result<Uuid, DbError>; 255 + 256 + async fn update_passkey_counter( 257 + &self, 258 + credential_id: &[u8], 259 + new_counter: i32, 260 + ) -> Result<bool, DbError>; 261 + 262 + async fn delete_passkey(&self, id: Uuid, did: &Did) -> Result<bool, DbError>; 263 + 264 + async fn update_passkey_name(&self, id: Uuid, did: &Did, name: &str) -> Result<bool, DbError>; 265 + 266 + async fn save_webauthn_challenge( 267 + &self, 268 + did: &Did, 269 + challenge_type: &str, 270 + state_json: &str, 271 + ) -> Result<Uuid, DbError>; 272 + 273 + async fn load_webauthn_challenge( 274 + &self, 275 + did: &Did, 276 + challenge_type: &str, 277 + ) -> Result<Option<String>, DbError>; 278 + 279 + async fn delete_webauthn_challenge(&self, did: &Did, challenge_type: &str) 280 + -> Result<(), DbError>; 281 + 282 + async fn get_totp_record(&self, did: &Did) -> Result<Option<TotpRecord>, DbError>; 283 + 284 + async fn upsert_totp_secret( 285 + &self, 286 + did: &Did, 287 + secret_encrypted: &[u8], 288 + encryption_version: i32, 289 + ) -> Result<(), DbError>; 290 + 291 + async fn set_totp_verified(&self, did: &Did) -> Result<(), DbError>; 292 + 293 + async fn update_totp_last_used(&self, did: &Did) -> Result<(), DbError>; 294 + 295 + async fn delete_totp(&self, did: &Did) -> Result<(), DbError>; 296 + 297 + async fn get_unused_backup_codes(&self, did: &Did) -> Result<Vec<StoredBackupCode>, DbError>; 298 + 299 + async fn mark_backup_code_used(&self, code_id: Uuid) -> Result<bool, DbError>; 300 + 301 + async fn count_unused_backup_codes(&self, did: &Did) -> Result<i64, DbError>; 302 + 303 + async fn delete_backup_codes(&self, did: &Did) -> Result<u64, DbError>; 304 + 305 + async fn insert_backup_codes(&self, did: &Did, code_hashes: &[String]) -> Result<(), DbError>; 306 + 307 + async fn enable_totp_with_backup_codes( 308 + &self, 309 + did: &Did, 310 + code_hashes: &[String], 311 + ) -> Result<(), DbError>; 312 + 313 + async fn delete_totp_and_backup_codes(&self, did: &Did) -> Result<(), DbError>; 314 + 315 + async fn replace_backup_codes(&self, did: &Did, code_hashes: &[String]) -> Result<(), DbError>; 316 + 317 + async fn get_session_info_by_did(&self, did: &Did) -> Result<Option<UserSessionInfo>, DbError>; 318 + 319 + async fn get_legacy_login_pref(&self, did: &Did) -> Result<Option<UserLegacyLoginPref>, DbError>; 320 + 321 + async fn update_legacy_login(&self, did: &Did, allow: bool) -> Result<bool, DbError>; 322 + 323 + async fn update_locale(&self, did: &Did, locale: &str) -> Result<bool, DbError>; 324 + 325 + async fn get_login_full_by_identifier( 326 + &self, 327 + identifier: &str, 328 + ) -> Result<Option<UserLoginFull>, DbError>; 329 + 330 + async fn get_confirm_signup_by_did( 331 + &self, 332 + did: &Did, 333 + ) -> Result<Option<UserConfirmSignup>, DbError>; 334 + 335 + async fn get_resend_verification_by_did( 336 + &self, 337 + did: &Did, 338 + ) -> Result<Option<UserResendVerification>, DbError>; 339 + 340 + async fn set_channel_verified(&self, did: &Did, channel: CommsChannel) -> Result<(), DbError>; 341 + 342 + async fn get_id_by_email_or_handle( 343 + &self, 344 + email: &str, 345 + handle: &str, 346 + ) -> Result<Option<Uuid>, DbError>; 347 + 348 + async fn set_password_reset_code( 349 + &self, 350 + user_id: Uuid, 351 + code: &str, 352 + expires_at: DateTime<Utc>, 353 + ) -> Result<(), DbError>; 354 + 355 + async fn get_user_by_reset_code( 356 + &self, 357 + code: &str, 358 + ) -> Result<Option<UserResetCodeInfo>, DbError>; 359 + 360 + async fn clear_password_reset_code(&self, user_id: Uuid) -> Result<(), DbError>; 361 + 362 + async fn get_id_and_password_hash_by_did( 363 + &self, 364 + did: &Did, 365 + ) -> Result<Option<UserIdAndPasswordHash>, DbError>; 366 + 367 + async fn update_password_hash(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError>; 368 + 369 + async fn reset_password_with_sessions( 370 + &self, 371 + user_id: Uuid, 372 + password_hash: &str, 373 + ) -> Result<PasswordResetResult, DbError>; 374 + 375 + async fn activate_account(&self, did: &Did) -> Result<bool, DbError>; 376 + 377 + async fn deactivate_account( 378 + &self, 379 + did: &Did, 380 + delete_after: Option<DateTime<Utc>>, 381 + ) -> Result<bool, DbError>; 382 + 383 + async fn has_password_by_did(&self, did: &Did) -> Result<Option<bool>, DbError>; 384 + 385 + async fn get_password_info_by_did( 386 + &self, 387 + did: &Did, 388 + ) -> Result<Option<UserPasswordInfo>, DbError>; 389 + 390 + async fn remove_user_password(&self, user_id: Uuid) -> Result<(), DbError>; 391 + 392 + async fn set_new_user_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError>; 393 + 394 + async fn get_user_key_by_did(&self, did: &Did) -> Result<Option<UserKeyInfo>, DbError>; 395 + 396 + async fn delete_account_complete( 397 + &self, 398 + user_id: Uuid, 399 + did: &Did, 400 + ) -> Result<(), DbError>; 401 + 402 + async fn set_user_takedown(&self, did: &Did, takedown_ref: Option<&str>) -> Result<bool, DbError>; 403 + 404 + async fn admin_delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError>; 405 + 406 + async fn get_user_for_did_doc(&self, did: &Did) -> Result<Option<UserForDidDoc>, DbError>; 407 + 408 + async fn get_user_for_did_doc_build(&self, did: &Did) -> Result<Option<UserForDidDocBuild>, DbError>; 409 + 410 + async fn upsert_did_web_overrides( 411 + &self, 412 + user_id: Uuid, 413 + verification_methods: Option<serde_json::Value>, 414 + also_known_as: Option<Vec<String>>, 415 + ) -> Result<(), DbError>; 416 + 417 + async fn update_migrated_to_pds( 418 + &self, 419 + did: &Did, 420 + endpoint: &str, 421 + ) -> Result<(), DbError>; 422 + 423 + async fn get_user_for_passkey_setup(&self, did: &Did) -> Result<Option<UserForPasskeySetup>, DbError>; 424 + 425 + async fn get_user_for_passkey_recovery( 426 + &self, 427 + identifier: &str, 428 + normalized_handle: &str, 429 + ) -> Result<Option<UserForPasskeyRecovery>, DbError>; 430 + 431 + async fn set_recovery_token( 432 + &self, 433 + did: &Did, 434 + token_hash: &str, 435 + expires_at: DateTime<Utc>, 436 + ) -> Result<(), DbError>; 437 + 438 + async fn get_user_for_recovery(&self, did: &Did) -> Result<Option<UserForRecovery>, DbError>; 439 + 440 + async fn get_accounts_scheduled_for_deletion( 441 + &self, 442 + limit: i64, 443 + ) -> Result<Vec<ScheduledDeletionAccount>, DbError>; 444 + 445 + async fn delete_account_with_firehose( 446 + &self, 447 + user_id: Uuid, 448 + did: &Did, 449 + ) -> Result<i64, DbError>; 450 + 451 + async fn create_password_account( 452 + &self, 453 + input: &CreatePasswordAccountInput, 454 + ) -> Result<CreatePasswordAccountResult, CreateAccountError>; 455 + 456 + async fn create_delegated_account( 457 + &self, 458 + input: &CreateDelegatedAccountInput, 459 + ) -> Result<Uuid, CreateAccountError>; 460 + 461 + async fn create_passkey_account( 462 + &self, 463 + input: &CreatePasskeyAccountInput, 464 + ) -> Result<CreatePasswordAccountResult, CreateAccountError>; 465 + 466 + async fn reactivate_migration_account( 467 + &self, 468 + input: &MigrationReactivationInput, 469 + ) -> Result<ReactivatedAccountInfo, MigrationReactivationError>; 470 + 471 + async fn check_handle_available_for_new_account(&self, handle: &Handle) -> Result<bool, DbError>; 472 + 473 + async fn check_and_consume_invite_code(&self, code: &str) -> Result<bool, DbError>; 474 + 475 + async fn complete_passkey_setup( 476 + &self, 477 + input: &CompletePasskeySetupInput, 478 + ) -> Result<(), DbError>; 479 + 480 + async fn recover_passkey_account( 481 + &self, 482 + input: &RecoverPasskeyAccountInput, 483 + ) -> Result<RecoverPasskeyAccountResult, DbError>; 484 + } 485 + 486 + #[derive(Debug, Clone)] 487 + pub struct UserKeyWithId { 488 + pub id: Uuid, 489 + pub key_bytes: Vec<u8>, 490 + pub encryption_version: Option<i32>, 491 + } 492 + 493 + #[derive(Debug, Clone)] 494 + pub struct UserKeyInfo { 495 + pub key_bytes: Vec<u8>, 496 + pub encryption_version: Option<i32>, 497 + } 498 + 499 + #[derive(Debug, Clone)] 500 + pub struct UserIdAndHandle { 501 + pub id: Uuid, 502 + pub handle: Handle, 503 + } 504 + 505 + #[derive(Debug, Clone)] 506 + pub struct UserDidWebInfo { 507 + pub id: Uuid, 508 + pub did: Did, 509 + pub migrated_to_pds: Option<String>, 510 + } 511 + 512 + #[derive(Debug, Clone)] 513 + pub struct DidWebOverrides { 514 + pub verification_methods: serde_json::Value, 515 + pub also_known_as: Vec<String>, 516 + } 517 + 518 + #[derive(Debug, Clone)] 519 + pub struct UserCommsPrefs { 520 + pub email: Option<String>, 521 + pub handle: Handle, 522 + pub preferred_channel: String, 523 + pub preferred_locale: Option<String>, 524 + } 525 + 526 + #[derive(Debug, Clone)] 527 + pub struct UserForVerification { 528 + pub id: Uuid, 529 + pub did: Did, 530 + pub email: Option<String>, 531 + pub email_verified: bool, 532 + pub handle: Handle, 533 + } 534 + 535 + #[derive(Debug, Clone)] 536 + pub struct OAuthTokenWithUser { 537 + pub did: Did, 538 + pub expires_at: DateTime<Utc>, 539 + pub deactivated_at: Option<DateTime<Utc>>, 540 + pub takedown_ref: Option<String>, 541 + pub is_admin: bool, 542 + pub key_bytes: Option<Vec<u8>>, 543 + pub encryption_version: Option<i32>, 544 + } 545 + 546 + #[derive(Debug, Clone)] 547 + pub struct UserInfoForAuth { 548 + pub deactivated_at: Option<DateTime<Utc>>, 549 + pub takedown_ref: Option<String>, 550 + pub is_admin: bool, 551 + pub key_bytes: Option<Vec<u8>>, 552 + pub encryption_version: Option<i32>, 553 + } 554 + 555 + #[derive(Debug, Clone)] 556 + pub struct AccountSearchResult { 557 + pub did: Did, 558 + pub handle: Handle, 559 + pub email: Option<String>, 560 + pub created_at: DateTime<Utc>, 561 + pub email_verified: bool, 562 + pub deactivated_at: Option<DateTime<Utc>>, 563 + pub invites_disabled: Option<bool>, 564 + } 565 + 566 + #[derive(Debug, Clone)] 567 + pub struct UserAuthInfo { 568 + pub id: Uuid, 569 + pub did: Did, 570 + pub password_hash: Option<String>, 571 + pub deactivated_at: Option<DateTime<Utc>>, 572 + pub takedown_ref: Option<String>, 573 + pub email_verified: bool, 574 + pub discord_verified: bool, 575 + pub telegram_verified: bool, 576 + pub signal_verified: bool, 577 + } 578 + 579 + #[derive(Debug, Clone)] 580 + pub struct NotificationPrefs { 581 + pub email: String, 582 + pub preferred_channel: String, 583 + pub discord_id: Option<String>, 584 + pub discord_verified: bool, 585 + pub telegram_username: Option<String>, 586 + pub telegram_verified: bool, 587 + pub signal_number: Option<String>, 588 + pub signal_verified: bool, 589 + } 590 + 591 + #[derive(Debug, Clone)] 592 + pub struct UserIdHandleEmail { 593 + pub id: Uuid, 594 + pub handle: Handle, 595 + pub email: Option<String>, 596 + } 597 + 598 + #[derive(Debug, Clone)] 599 + pub struct UserVerificationInfo { 600 + pub id: Uuid, 601 + pub handle: Handle, 602 + pub email: Option<String>, 603 + pub email_verified: bool, 604 + pub discord_verified: bool, 605 + pub telegram_verified: bool, 606 + pub signal_verified: bool, 607 + } 608 + 609 + #[derive(Debug, Clone)] 610 + pub struct StoredPasskey { 611 + pub id: Uuid, 612 + pub did: Did, 613 + pub credential_id: Vec<u8>, 614 + pub public_key: Vec<u8>, 615 + pub sign_count: i32, 616 + pub created_at: DateTime<Utc>, 617 + pub last_used: Option<DateTime<Utc>>, 618 + pub friendly_name: Option<String>, 619 + pub aaguid: Option<Vec<u8>>, 620 + pub transports: Option<Vec<String>>, 621 + } 622 + 623 + impl StoredPasskey { 624 + pub fn credential_id_base64(&self) -> String { 625 + use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; 626 + URL_SAFE_NO_PAD.encode(&self.credential_id) 627 + } 628 + } 629 + 630 + #[derive(Debug, Clone)] 631 + pub struct TotpRecord { 632 + pub secret_encrypted: Vec<u8>, 633 + pub encryption_version: i32, 634 + pub verified: bool, 635 + } 636 + 637 + #[derive(Debug, Clone)] 638 + pub struct StoredBackupCode { 639 + pub id: Uuid, 640 + pub code_hash: String, 641 + } 642 + 643 + #[derive(Debug, Clone)] 644 + pub struct UserSessionInfo { 645 + pub handle: Handle, 646 + pub email: Option<String>, 647 + pub email_verified: bool, 648 + pub is_admin: bool, 649 + pub deactivated_at: Option<DateTime<Utc>>, 650 + pub takedown_ref: Option<String>, 651 + pub preferred_locale: Option<String>, 652 + pub preferred_comms_channel: CommsChannel, 653 + pub discord_verified: bool, 654 + pub telegram_verified: bool, 655 + pub signal_verified: bool, 656 + pub migrated_to_pds: Option<String>, 657 + pub migrated_at: Option<DateTime<Utc>>, 658 + } 659 + 660 + #[derive(Debug, Clone)] 661 + pub struct UserLegacyLoginPref { 662 + pub allow_legacy_login: bool, 663 + pub has_mfa: bool, 664 + } 665 + 666 + #[derive(Debug, Clone)] 667 + pub struct UserLoginFull { 668 + pub id: Uuid, 669 + pub did: Did, 670 + pub handle: Handle, 671 + pub password_hash: Option<String>, 672 + pub email: Option<String>, 673 + pub deactivated_at: Option<DateTime<Utc>>, 674 + pub takedown_ref: Option<String>, 675 + pub email_verified: bool, 676 + pub discord_verified: bool, 677 + pub telegram_verified: bool, 678 + pub signal_verified: bool, 679 + pub allow_legacy_login: bool, 680 + pub migrated_to_pds: Option<String>, 681 + pub preferred_comms_channel: CommsChannel, 682 + pub key_bytes: Vec<u8>, 683 + pub encryption_version: Option<i32>, 684 + pub totp_enabled: bool, 685 + } 686 + 687 + #[derive(Debug, Clone)] 688 + pub struct UserConfirmSignup { 689 + pub id: Uuid, 690 + pub did: Did, 691 + pub handle: Handle, 692 + pub email: Option<String>, 693 + pub channel: CommsChannel, 694 + pub discord_id: Option<String>, 695 + pub telegram_username: Option<String>, 696 + pub signal_number: Option<String>, 697 + pub key_bytes: Vec<u8>, 698 + pub encryption_version: Option<i32>, 699 + } 700 + 701 + #[derive(Debug, Clone)] 702 + pub struct UserResendVerification { 703 + pub id: Uuid, 704 + pub handle: Handle, 705 + pub email: Option<String>, 706 + pub channel: CommsChannel, 707 + pub discord_id: Option<String>, 708 + pub telegram_username: Option<String>, 709 + pub signal_number: Option<String>, 710 + pub email_verified: bool, 711 + pub discord_verified: bool, 712 + pub telegram_verified: bool, 713 + pub signal_verified: bool, 714 + } 715 + 716 + #[derive(Debug, Clone)] 717 + pub struct UserResetCodeInfo { 718 + pub id: Uuid, 719 + pub expires_at: Option<DateTime<Utc>>, 720 + } 721 + 722 + #[derive(Debug, Clone)] 723 + pub struct UserPasswordInfo { 724 + pub id: Uuid, 725 + pub password_hash: Option<String>, 726 + } 727 + 728 + #[derive(Debug, Clone)] 729 + pub struct UserIdAndPasswordHash { 730 + pub id: Uuid, 731 + pub password_hash: String, 732 + } 733 + 734 + #[derive(Debug, Clone)] 735 + pub struct PasswordResetResult { 736 + pub did: Did, 737 + pub session_jtis: Vec<String>, 738 + } 739 + 740 + #[derive(Debug, Clone)] 741 + pub struct UserForDeletion { 742 + pub id: Uuid, 743 + pub password_hash: Option<String>, 744 + pub handle: Handle, 745 + } 746 + 747 + #[derive(Debug, Clone)] 748 + pub struct ScheduledDeletionAccount { 749 + pub id: Uuid, 750 + pub did: Did, 751 + pub handle: Handle, 752 + } 753 + 754 + #[derive(Debug, Clone)] 755 + pub struct UserForDidDoc { 756 + pub id: Uuid, 757 + pub handle: Handle, 758 + pub deactivated_at: Option<DateTime<Utc>>, 759 + } 760 + 761 + #[derive(Debug, Clone)] 762 + pub struct UserForDidDocBuild { 763 + pub id: Uuid, 764 + pub handle: Handle, 765 + pub migrated_to_pds: Option<String>, 766 + } 767 + 768 + #[derive(Debug, Clone)] 769 + pub struct UserForPasskeySetup { 770 + pub id: Uuid, 771 + pub handle: Handle, 772 + pub recovery_token: Option<String>, 773 + pub recovery_token_expires_at: Option<DateTime<Utc>>, 774 + pub password_required: bool, 775 + } 776 + 777 + #[derive(Debug, Clone)] 778 + pub struct UserForPasskeyRecovery { 779 + pub id: Uuid, 780 + pub did: Did, 781 + pub handle: Handle, 782 + pub password_required: bool, 783 + } 784 + 785 + #[derive(Debug, Clone)] 786 + pub struct UserForRecovery { 787 + pub id: Uuid, 788 + pub did: Did, 789 + pub recovery_token: Option<String>, 790 + pub recovery_token_expires_at: Option<DateTime<Utc>>, 791 + } 792 + 793 + #[derive(Debug, Clone)] 794 + pub struct CreatePasswordAccountInput { 795 + pub handle: Handle, 796 + pub email: Option<String>, 797 + pub did: Did, 798 + pub password_hash: String, 799 + pub preferred_comms_channel: CommsChannel, 800 + pub discord_id: Option<String>, 801 + pub telegram_username: Option<String>, 802 + pub signal_number: Option<String>, 803 + pub deactivated_at: Option<DateTime<Utc>>, 804 + pub encrypted_key_bytes: Vec<u8>, 805 + pub encryption_version: i32, 806 + pub reserved_key_id: Option<Uuid>, 807 + pub commit_cid: String, 808 + pub repo_rev: String, 809 + pub genesis_block_cids: Vec<Vec<u8>>, 810 + pub invite_code: Option<String>, 811 + pub birthdate_pref: Option<serde_json::Value>, 812 + } 813 + 814 + #[derive(Debug, Clone, Default)] 815 + pub struct CreatePasswordAccountResult { 816 + pub user_id: Uuid, 817 + pub is_admin: bool, 818 + } 819 + 820 + #[derive(Debug, Clone)] 821 + pub enum CreateAccountError { 822 + HandleTaken, 823 + EmailTaken, 824 + DidExists, 825 + Database(String), 826 + } 827 + 828 + #[derive(Debug, Clone)] 829 + pub struct CreateDelegatedAccountInput { 830 + pub handle: Handle, 831 + pub email: Option<String>, 832 + pub did: Did, 833 + pub controller_did: Did, 834 + pub controller_scopes: String, 835 + pub encrypted_key_bytes: Vec<u8>, 836 + pub encryption_version: i32, 837 + pub commit_cid: String, 838 + pub repo_rev: String, 839 + pub genesis_block_cids: Vec<Vec<u8>>, 840 + pub invite_code: Option<String>, 841 + } 842 + 843 + #[derive(Debug, Clone)] 844 + pub struct CreatePasskeyAccountInput { 845 + pub handle: Handle, 846 + pub email: String, 847 + pub did: Did, 848 + pub preferred_comms_channel: CommsChannel, 849 + pub discord_id: Option<String>, 850 + pub telegram_username: Option<String>, 851 + pub signal_number: Option<String>, 852 + pub setup_token_hash: String, 853 + pub setup_expires_at: DateTime<Utc>, 854 + pub deactivated_at: Option<DateTime<Utc>>, 855 + pub encrypted_key_bytes: Vec<u8>, 856 + pub encryption_version: i32, 857 + pub reserved_key_id: Option<Uuid>, 858 + pub commit_cid: String, 859 + pub repo_rev: String, 860 + pub genesis_block_cids: Vec<Vec<u8>>, 861 + pub invite_code: Option<String>, 862 + pub birthdate_pref: Option<serde_json::Value>, 863 + } 864 + 865 + #[derive(Debug, Clone)] 866 + pub struct CompletePasskeySetupInput { 867 + pub user_id: Uuid, 868 + pub did: Did, 869 + pub app_password_name: String, 870 + pub app_password_hash: String, 871 + } 872 + 873 + #[derive(Debug, Clone)] 874 + pub struct RecoverPasskeyAccountInput { 875 + pub did: Did, 876 + pub password_hash: String, 877 + } 878 + 879 + #[derive(Debug, Clone)] 880 + pub struct RecoverPasskeyAccountResult { 881 + pub passkeys_deleted: u64, 882 + } 883 + 884 + #[derive(Debug, Clone)] 885 + pub struct MigrationReactivationInput { 886 + pub did: Did, 887 + pub new_handle: Handle, 888 + } 889 + 890 + #[derive(Debug, Clone)] 891 + pub struct ReactivatedAccountInfo { 892 + pub user_id: Uuid, 893 + pub old_handle: Handle, 894 + } 895 + 896 + #[derive(Debug, Clone)] 897 + pub enum MigrationReactivationError { 898 + NotFound, 899 + NotDeactivated, 900 + HandleTaken, 901 + Database(String), 902 + }
+25
crates/tranquil-db/Cargo.toml
··· 1 + [package] 2 + name = "tranquil-db" 3 + version.workspace = true 4 + edition.workspace = true 5 + license.workspace = true 6 + 7 + [features] 8 + default = ["postgres"] 9 + postgres = [] 10 + sqlite = [] 11 + 12 + [dependencies] 13 + tranquil-db-traits = { workspace = true } 14 + tranquil-oauth = { workspace = true } 15 + tranquil-types = { workspace = true } 16 + async-trait = { workspace = true } 17 + chrono = { workspace = true } 18 + rand = { workspace = true } 19 + serde = { workspace = true } 20 + serde_json = { workspace = true } 21 + thiserror = { workspace = true } 22 + tracing = { workspace = true } 23 + uuid = { workspace = true } 24 + 25 + sqlx = { workspace = true }
+7
crates/tranquil-db/src/lib.rs
··· 1 + #[cfg(feature = "postgres")] 2 + pub mod postgres; 3 + 4 + pub use tranquil_db_traits::*; 5 + 6 + #[cfg(feature = "postgres")] 7 + pub use postgres::PostgresRepositories;
+99
crates/tranquil-db/src/postgres/backlink.rs
··· 1 + use async_trait::async_trait; 2 + use sqlx::PgPool; 3 + use tranquil_db_traits::{Backlink, BacklinkRepository, DbError}; 4 + use tranquil_types::{AtUri, Nsid}; 5 + use uuid::Uuid; 6 + 7 + use super::user::map_sqlx_error; 8 + 9 + pub struct PostgresBacklinkRepository { 10 + pool: PgPool, 11 + } 12 + 13 + impl PostgresBacklinkRepository { 14 + pub fn new(pool: PgPool) -> Self { 15 + Self { pool } 16 + } 17 + } 18 + 19 + #[async_trait] 20 + impl BacklinkRepository for PostgresBacklinkRepository { 21 + async fn get_backlink_conflicts( 22 + &self, 23 + repo_id: Uuid, 24 + collection: &Nsid, 25 + backlinks: &[Backlink], 26 + ) -> Result<Vec<AtUri>, DbError> { 27 + if backlinks.is_empty() { 28 + return Ok(Vec::new()); 29 + } 30 + 31 + let paths: Vec<&str> = backlinks.iter().map(|b| b.path.as_str()).collect(); 32 + let link_tos: Vec<&str> = backlinks.iter().map(|b| b.link_to.as_str()).collect(); 33 + let collection_pattern = format!("%/{}/%", collection.as_str()); 34 + 35 + let results = sqlx::query_scalar!( 36 + r#" 37 + SELECT DISTINCT uri 38 + FROM backlinks 39 + WHERE repo_id = $1 40 + AND uri LIKE $4 41 + AND (path, link_to) IN (SELECT unnest($2::text[]), unnest($3::text[])) 42 + "#, 43 + repo_id, 44 + &paths as &[&str], 45 + &link_tos as &[&str], 46 + collection_pattern 47 + ) 48 + .fetch_all(&self.pool) 49 + .await 50 + .map_err(map_sqlx_error)?; 51 + 52 + Ok(results.into_iter().map(Into::into).collect()) 53 + } 54 + 55 + async fn add_backlinks(&self, repo_id: Uuid, backlinks: &[Backlink]) -> Result<(), DbError> { 56 + if backlinks.is_empty() { 57 + return Ok(()); 58 + } 59 + 60 + let uris: Vec<&str> = backlinks.iter().map(|b| b.uri.as_str()).collect(); 61 + let paths: Vec<&str> = backlinks.iter().map(|b| b.path.as_str()).collect(); 62 + let link_tos: Vec<&str> = backlinks.iter().map(|b| b.link_to.as_str()).collect(); 63 + 64 + sqlx::query!( 65 + r#" 66 + INSERT INTO backlinks (uri, path, link_to, repo_id) 67 + SELECT unnest($1::text[]), unnest($2::text[]), unnest($3::text[]), $4 68 + ON CONFLICT (uri, path) DO NOTHING 69 + "#, 70 + &uris as &[&str], 71 + &paths as &[&str], 72 + &link_tos as &[&str], 73 + repo_id 74 + ) 75 + .execute(&self.pool) 76 + .await 77 + .map_err(map_sqlx_error)?; 78 + 79 + Ok(()) 80 + } 81 + 82 + async fn remove_backlinks_by_uri(&self, uri: &AtUri) -> Result<(), DbError> { 83 + sqlx::query!("DELETE FROM backlinks WHERE uri = $1", uri.as_str()) 84 + .execute(&self.pool) 85 + .await 86 + .map_err(map_sqlx_error)?; 87 + 88 + Ok(()) 89 + } 90 + 91 + async fn remove_backlinks_by_repo(&self, repo_id: Uuid) -> Result<(), DbError> { 92 + sqlx::query!("DELETE FROM backlinks WHERE repo_id = $1", repo_id) 93 + .execute(&self.pool) 94 + .await 95 + .map_err(map_sqlx_error)?; 96 + 97 + Ok(()) 98 + } 99 + }
+299
crates/tranquil-db/src/postgres/backup.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use sqlx::PgPool; 4 + use tranquil_db_traits::{ 5 + BackupForDeletion, BackupRepository, BackupRow, BackupStorageInfo, BlobExportInfo, DbError, 6 + OldBackupInfo, UserBackupInfo, 7 + }; 8 + use tranquil_types::Did; 9 + use uuid::Uuid; 10 + 11 + use super::user::map_sqlx_error; 12 + 13 + pub struct PostgresBackupRepository { 14 + pool: PgPool, 15 + } 16 + 17 + impl PostgresBackupRepository { 18 + pub fn new(pool: PgPool) -> Self { 19 + Self { pool } 20 + } 21 + } 22 + 23 + #[async_trait] 24 + impl BackupRepository for PostgresBackupRepository { 25 + async fn get_user_backup_status(&self, did: &Did) -> Result<Option<(Uuid, bool)>, DbError> { 26 + let result = sqlx::query!( 27 + "SELECT id, backup_enabled FROM users WHERE did = $1", 28 + did.as_str() 29 + ) 30 + .fetch_optional(&self.pool) 31 + .await 32 + .map_err(map_sqlx_error)?; 33 + 34 + Ok(result.map(|r| (r.id, r.backup_enabled))) 35 + } 36 + 37 + async fn list_backups_for_user(&self, user_id: Uuid) -> Result<Vec<BackupRow>, DbError> { 38 + let results = sqlx::query_as!( 39 + BackupRow, 40 + r#" 41 + SELECT id, repo_rev, repo_root_cid, block_count, size_bytes, created_at 42 + FROM account_backups 43 + WHERE user_id = $1 44 + ORDER BY created_at DESC 45 + "#, 46 + user_id 47 + ) 48 + .fetch_all(&self.pool) 49 + .await 50 + .map_err(map_sqlx_error)?; 51 + 52 + Ok(results) 53 + } 54 + 55 + async fn get_backup_storage_info( 56 + &self, 57 + backup_id: Uuid, 58 + did: &Did, 59 + ) -> Result<Option<BackupStorageInfo>, DbError> { 60 + let result = sqlx::query!( 61 + r#" 62 + SELECT ab.storage_key, ab.repo_rev 63 + FROM account_backups ab 64 + JOIN users u ON u.id = ab.user_id 65 + WHERE ab.id = $1 AND u.did = $2 66 + "#, 67 + backup_id, 68 + did.as_str() 69 + ) 70 + .fetch_optional(&self.pool) 71 + .await 72 + .map_err(map_sqlx_error)?; 73 + 74 + Ok(result.map(|r| BackupStorageInfo { 75 + storage_key: r.storage_key, 76 + repo_rev: r.repo_rev, 77 + })) 78 + } 79 + 80 + async fn get_user_for_backup(&self, did: &Did) -> Result<Option<UserBackupInfo>, DbError> { 81 + let result = sqlx::query!( 82 + r#" 83 + SELECT u.id, u.did, u.backup_enabled, u.deactivated_at, r.repo_root_cid, r.repo_rev 84 + FROM users u 85 + JOIN repos r ON r.user_id = u.id 86 + WHERE u.did = $1 87 + "#, 88 + did.as_str() 89 + ) 90 + .fetch_optional(&self.pool) 91 + .await 92 + .map_err(map_sqlx_error)?; 93 + 94 + Ok(result.map(|r| UserBackupInfo { 95 + id: r.id, 96 + did: r.did.into(), 97 + backup_enabled: r.backup_enabled, 98 + deactivated_at: r.deactivated_at, 99 + repo_root_cid: r.repo_root_cid, 100 + repo_rev: r.repo_rev, 101 + })) 102 + } 103 + 104 + async fn insert_backup( 105 + &self, 106 + user_id: Uuid, 107 + storage_key: &str, 108 + repo_root_cid: &str, 109 + repo_rev: &str, 110 + block_count: i32, 111 + size_bytes: i64, 112 + ) -> Result<Uuid, DbError> { 113 + let id = sqlx::query_scalar!( 114 + r#" 115 + INSERT INTO account_backups (user_id, storage_key, repo_root_cid, repo_rev, block_count, size_bytes) 116 + VALUES ($1, $2, $3, $4, $5, $6) 117 + RETURNING id 118 + "#, 119 + user_id, 120 + storage_key, 121 + repo_root_cid, 122 + repo_rev, 123 + block_count, 124 + size_bytes 125 + ) 126 + .fetch_one(&self.pool) 127 + .await 128 + .map_err(map_sqlx_error)?; 129 + 130 + Ok(id) 131 + } 132 + 133 + async fn get_old_backups( 134 + &self, 135 + user_id: Uuid, 136 + retention_offset: i64, 137 + ) -> Result<Vec<OldBackupInfo>, DbError> { 138 + let results = sqlx::query!( 139 + r#" 140 + SELECT id, storage_key 141 + FROM account_backups 142 + WHERE user_id = $1 143 + ORDER BY created_at DESC 144 + OFFSET $2 145 + "#, 146 + user_id, 147 + retention_offset 148 + ) 149 + .fetch_all(&self.pool) 150 + .await 151 + .map_err(map_sqlx_error)?; 152 + 153 + Ok(results 154 + .into_iter() 155 + .map(|r| OldBackupInfo { 156 + id: r.id, 157 + storage_key: r.storage_key, 158 + }) 159 + .collect()) 160 + } 161 + 162 + async fn delete_backup(&self, backup_id: Uuid) -> Result<(), DbError> { 163 + sqlx::query!("DELETE FROM account_backups WHERE id = $1", backup_id) 164 + .execute(&self.pool) 165 + .await 166 + .map_err(map_sqlx_error)?; 167 + 168 + Ok(()) 169 + } 170 + 171 + async fn get_backup_for_deletion( 172 + &self, 173 + backup_id: Uuid, 174 + did: &Did, 175 + ) -> Result<Option<BackupForDeletion>, DbError> { 176 + let result = sqlx::query!( 177 + r#" 178 + SELECT ab.id, ab.storage_key, u.deactivated_at 179 + FROM account_backups ab 180 + JOIN users u ON u.id = ab.user_id 181 + WHERE ab.id = $1 AND u.did = $2 182 + "#, 183 + backup_id, 184 + did.as_str() 185 + ) 186 + .fetch_optional(&self.pool) 187 + .await 188 + .map_err(map_sqlx_error)?; 189 + 190 + Ok(result.map(|r| BackupForDeletion { 191 + id: r.id, 192 + storage_key: r.storage_key, 193 + deactivated_at: r.deactivated_at, 194 + })) 195 + } 196 + 197 + async fn get_user_deactivated_status( 198 + &self, 199 + did: &Did, 200 + ) -> Result<Option<Option<DateTime<Utc>>>, DbError> { 201 + let result = sqlx::query!( 202 + "SELECT deactivated_at FROM users WHERE did = $1", 203 + did.as_str() 204 + ) 205 + .fetch_optional(&self.pool) 206 + .await 207 + .map_err(map_sqlx_error)?; 208 + 209 + Ok(result.map(|r| r.deactivated_at)) 210 + } 211 + 212 + async fn update_backup_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError> { 213 + sqlx::query!( 214 + "UPDATE users SET backup_enabled = $1 WHERE did = $2", 215 + enabled, 216 + did.as_str() 217 + ) 218 + .execute(&self.pool) 219 + .await 220 + .map_err(map_sqlx_error)?; 221 + 222 + Ok(()) 223 + } 224 + 225 + async fn get_user_id_by_did(&self, did: &Did) -> Result<Option<Uuid>, DbError> { 226 + let result = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str()) 227 + .fetch_optional(&self.pool) 228 + .await 229 + .map_err(map_sqlx_error)?; 230 + 231 + Ok(result) 232 + } 233 + 234 + async fn get_blobs_for_export(&self, user_id: Uuid) -> Result<Vec<BlobExportInfo>, DbError> { 235 + let results = sqlx::query!( 236 + r#" 237 + SELECT DISTINCT b.cid, b.storage_key, b.mime_type 238 + FROM blobs b 239 + JOIN record_blobs rb ON rb.blob_cid = b.cid 240 + WHERE rb.repo_id = $1 241 + "#, 242 + user_id 243 + ) 244 + .fetch_all(&self.pool) 245 + .await 246 + .map_err(map_sqlx_error)?; 247 + 248 + Ok(results 249 + .into_iter() 250 + .map(|r| BlobExportInfo { 251 + cid: r.cid, 252 + storage_key: r.storage_key, 253 + mime_type: r.mime_type, 254 + }) 255 + .collect()) 256 + } 257 + 258 + async fn get_users_needing_backup( 259 + &self, 260 + backup_interval_secs: i64, 261 + limit: i64, 262 + ) -> Result<Vec<UserBackupInfo>, DbError> { 263 + let results = sqlx::query!( 264 + r#" 265 + SELECT u.id, u.did, u.backup_enabled, u.deactivated_at, r.repo_root_cid, r.repo_rev 266 + FROM users u 267 + JOIN repos r ON r.user_id = u.id 268 + WHERE u.backup_enabled = true 269 + AND u.deactivated_at IS NULL 270 + AND ( 271 + NOT EXISTS ( 272 + SELECT 1 FROM account_backups ab WHERE ab.user_id = u.id 273 + ) 274 + OR ( 275 + SELECT MAX(ab.created_at) FROM account_backups ab WHERE ab.user_id = u.id 276 + ) < NOW() - make_interval(secs => $1) 277 + ) 278 + LIMIT $2 279 + "#, 280 + backup_interval_secs as f64, 281 + limit 282 + ) 283 + .fetch_all(&self.pool) 284 + .await 285 + .map_err(map_sqlx_error)?; 286 + 287 + Ok(results 288 + .into_iter() 289 + .map(|r| UserBackupInfo { 290 + id: r.id, 291 + did: r.did.into(), 292 + backup_enabled: r.backup_enabled, 293 + deactivated_at: r.deactivated_at, 294 + repo_root_cid: r.repo_root_cid, 295 + repo_rev: r.repo_rev, 296 + }) 297 + .collect()) 298 + } 299 + }
+295
crates/tranquil-db/src/postgres/blob.rs
··· 1 + use async_trait::async_trait; 2 + use sqlx::PgPool; 3 + use tranquil_db_traits::{ 4 + BlobForExport, BlobMetadata, BlobRepository, BlobWithTakedown, DbError, MissingBlobInfo, 5 + }; 6 + use tranquil_types::{AtUri, CidLink, Did}; 7 + use uuid::Uuid; 8 + 9 + use super::user::map_sqlx_error; 10 + 11 + pub struct PostgresBlobRepository { 12 + pool: PgPool, 13 + } 14 + 15 + impl PostgresBlobRepository { 16 + pub fn new(pool: PgPool) -> Self { 17 + Self { pool } 18 + } 19 + } 20 + 21 + #[async_trait] 22 + impl BlobRepository for PostgresBlobRepository { 23 + async fn insert_blob( 24 + &self, 25 + cid: &CidLink, 26 + mime_type: &str, 27 + size_bytes: i64, 28 + created_by_user: Uuid, 29 + storage_key: &str, 30 + ) -> Result<Option<CidLink>, DbError> { 31 + let result = sqlx::query_scalar!( 32 + r#"INSERT INTO blobs (cid, mime_type, size_bytes, created_by_user, storage_key) 33 + VALUES ($1, $2, $3, $4, $5) 34 + ON CONFLICT (cid) DO NOTHING RETURNING cid"#, 35 + cid.as_str(), 36 + mime_type, 37 + size_bytes, 38 + created_by_user, 39 + storage_key 40 + ) 41 + .fetch_optional(&self.pool) 42 + .await 43 + .map_err(map_sqlx_error)?; 44 + 45 + Ok(result.map(CidLink::from)) 46 + } 47 + 48 + async fn get_blob_metadata(&self, cid: &CidLink) -> Result<Option<BlobMetadata>, DbError> { 49 + let result = sqlx::query!( 50 + "SELECT storage_key, mime_type, size_bytes FROM blobs WHERE cid = $1", 51 + cid.as_str() 52 + ) 53 + .fetch_optional(&self.pool) 54 + .await 55 + .map_err(map_sqlx_error)?; 56 + 57 + Ok(result.map(|r| BlobMetadata { 58 + storage_key: r.storage_key, 59 + mime_type: r.mime_type, 60 + size_bytes: r.size_bytes, 61 + })) 62 + } 63 + 64 + async fn get_blob_with_takedown( 65 + &self, 66 + cid: &CidLink, 67 + ) -> Result<Option<BlobWithTakedown>, DbError> { 68 + let result = sqlx::query!( 69 + "SELECT cid, takedown_ref FROM blobs WHERE cid = $1", 70 + cid.as_str() 71 + ) 72 + .fetch_optional(&self.pool) 73 + .await 74 + .map_err(map_sqlx_error)?; 75 + 76 + Ok(result.map(|r| BlobWithTakedown { 77 + cid: CidLink::from(r.cid), 78 + takedown_ref: r.takedown_ref, 79 + })) 80 + } 81 + 82 + async fn get_blob_storage_key(&self, cid: &CidLink) -> Result<Option<String>, DbError> { 83 + let result = sqlx::query_scalar!( 84 + "SELECT storage_key FROM blobs WHERE cid = $1", 85 + cid.as_str() 86 + ) 87 + .fetch_optional(&self.pool) 88 + .await 89 + .map_err(map_sqlx_error)?; 90 + 91 + Ok(result) 92 + } 93 + 94 + async fn list_blobs_by_user( 95 + &self, 96 + user_id: Uuid, 97 + cursor: Option<&str>, 98 + limit: i64, 99 + ) -> Result<Vec<CidLink>, DbError> { 100 + let cursor_val = cursor.unwrap_or(""); 101 + let results = sqlx::query_scalar!( 102 + r#"SELECT cid FROM blobs 103 + WHERE created_by_user = $1 AND cid > $2 104 + ORDER BY cid ASC 105 + LIMIT $3"#, 106 + user_id, 107 + cursor_val, 108 + limit 109 + ) 110 + .fetch_all(&self.pool) 111 + .await 112 + .map_err(map_sqlx_error)?; 113 + 114 + Ok(results.into_iter().map(CidLink::from).collect()) 115 + } 116 + 117 + async fn list_blobs_since_rev( 118 + &self, 119 + did: &Did, 120 + since: &str, 121 + ) -> Result<Vec<CidLink>, DbError> { 122 + let results = sqlx::query_scalar!( 123 + r#"SELECT DISTINCT unnest(blobs) as "cid!" 124 + FROM repo_seq 125 + WHERE did = $1 AND rev > $2 AND blobs IS NOT NULL"#, 126 + did.as_str(), 127 + since 128 + ) 129 + .fetch_all(&self.pool) 130 + .await 131 + .map_err(map_sqlx_error)?; 132 + 133 + Ok(results.into_iter().map(CidLink::from).collect()) 134 + } 135 + 136 + async fn count_blobs_by_user(&self, user_id: Uuid) -> Result<i64, DbError> { 137 + let result = sqlx::query_scalar!( 138 + r#"SELECT COUNT(*) as "count!" FROM blobs WHERE created_by_user = $1"#, 139 + user_id 140 + ) 141 + .fetch_one(&self.pool) 142 + .await 143 + .map_err(map_sqlx_error)?; 144 + 145 + Ok(result) 146 + } 147 + 148 + async fn sum_blob_storage(&self) -> Result<i64, DbError> { 149 + let result = sqlx::query_scalar!( 150 + r#"SELECT COALESCE(SUM(size_bytes), 0)::BIGINT as "total!" FROM blobs"# 151 + ) 152 + .fetch_one(&self.pool) 153 + .await 154 + .map_err(map_sqlx_error)?; 155 + 156 + Ok(result) 157 + } 158 + 159 + async fn update_blob_takedown( 160 + &self, 161 + cid: &CidLink, 162 + takedown_ref: Option<&str>, 163 + ) -> Result<bool, DbError> { 164 + let result = sqlx::query!( 165 + "UPDATE blobs SET takedown_ref = $1 WHERE cid = $2", 166 + takedown_ref, 167 + cid.as_str() 168 + ) 169 + .execute(&self.pool) 170 + .await 171 + .map_err(map_sqlx_error)?; 172 + 173 + Ok(result.rows_affected() > 0) 174 + } 175 + 176 + async fn delete_blob_by_cid(&self, cid: &CidLink) -> Result<bool, DbError> { 177 + let result = sqlx::query!("DELETE FROM blobs WHERE cid = $1", cid.as_str()) 178 + .execute(&self.pool) 179 + .await 180 + .map_err(map_sqlx_error)?; 181 + 182 + Ok(result.rows_affected() > 0) 183 + } 184 + 185 + async fn delete_blobs_by_user(&self, user_id: Uuid) -> Result<u64, DbError> { 186 + let result = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) 187 + .execute(&self.pool) 188 + .await 189 + .map_err(map_sqlx_error)?; 190 + 191 + Ok(result.rows_affected()) 192 + } 193 + 194 + async fn get_blob_storage_keys_by_user(&self, user_id: Uuid) -> Result<Vec<String>, DbError> { 195 + let results = sqlx::query_scalar!( 196 + r#"SELECT storage_key as "storage_key!" FROM blobs WHERE created_by_user = $1"#, 197 + user_id 198 + ) 199 + .fetch_all(&self.pool) 200 + .await 201 + .map_err(map_sqlx_error)?; 202 + 203 + Ok(results) 204 + } 205 + 206 + async fn insert_record_blobs( 207 + &self, 208 + repo_id: Uuid, 209 + record_uris: &[AtUri], 210 + blob_cids: &[CidLink], 211 + ) -> Result<(), DbError> { 212 + let uris_str: Vec<&str> = record_uris.iter().map(|u| u.as_str()).collect(); 213 + let cids_str: Vec<&str> = blob_cids.iter().map(|c| c.as_str()).collect(); 214 + 215 + sqlx::query!( 216 + r#"INSERT INTO record_blobs (repo_id, record_uri, blob_cid) 217 + SELECT $1, record_uri, blob_cid 218 + FROM UNNEST($2::text[], $3::text[]) AS t(record_uri, blob_cid) 219 + ON CONFLICT (repo_id, record_uri, blob_cid) DO NOTHING"#, 220 + repo_id, 221 + &uris_str as &[&str], 222 + &cids_str as &[&str] 223 + ) 224 + .execute(&self.pool) 225 + .await 226 + .map_err(map_sqlx_error)?; 227 + 228 + Ok(()) 229 + } 230 + 231 + async fn list_missing_blobs( 232 + &self, 233 + repo_id: Uuid, 234 + cursor: Option<&str>, 235 + limit: i64, 236 + ) -> Result<Vec<MissingBlobInfo>, DbError> { 237 + let cursor_val = cursor.unwrap_or(""); 238 + let results = sqlx::query!( 239 + r#"SELECT rb.blob_cid, rb.record_uri 240 + FROM record_blobs rb 241 + LEFT JOIN blobs b ON rb.blob_cid = b.cid 242 + WHERE rb.repo_id = $1 AND b.cid IS NULL AND rb.blob_cid > $2 243 + ORDER BY rb.blob_cid 244 + LIMIT $3"#, 245 + repo_id, 246 + cursor_val, 247 + limit 248 + ) 249 + .fetch_all(&self.pool) 250 + .await 251 + .map_err(map_sqlx_error)?; 252 + 253 + Ok(results 254 + .into_iter() 255 + .map(|r| MissingBlobInfo { 256 + blob_cid: CidLink::from(r.blob_cid), 257 + record_uri: AtUri::from(r.record_uri), 258 + }) 259 + .collect()) 260 + } 261 + 262 + async fn count_distinct_record_blobs(&self, repo_id: Uuid) -> Result<i64, DbError> { 263 + let result = sqlx::query_scalar!( 264 + r#"SELECT COUNT(DISTINCT blob_cid) as "count!" FROM record_blobs WHERE repo_id = $1"#, 265 + repo_id 266 + ) 267 + .fetch_one(&self.pool) 268 + .await 269 + .map_err(map_sqlx_error)?; 270 + 271 + Ok(result) 272 + } 273 + 274 + async fn get_blobs_for_export(&self, repo_id: Uuid) -> Result<Vec<BlobForExport>, DbError> { 275 + let results = sqlx::query!( 276 + r#"SELECT DISTINCT b.cid, b.storage_key, b.mime_type 277 + FROM blobs b 278 + JOIN record_blobs rb ON rb.blob_cid = b.cid 279 + WHERE rb.repo_id = $1"#, 280 + repo_id 281 + ) 282 + .fetch_all(&self.pool) 283 + .await 284 + .map_err(map_sqlx_error)?; 285 + 286 + Ok(results 287 + .into_iter() 288 + .map(|r| BlobForExport { 289 + cid: CidLink::from(r.cid), 290 + storage_key: r.storage_key, 291 + mime_type: r.mime_type, 292 + }) 293 + .collect()) 294 + } 295 + }
+476
crates/tranquil-db/src/postgres/delegation.rs
··· 1 + use async_trait::async_trait; 2 + use sqlx::PgPool; 3 + use tranquil_db_traits::{ 4 + AuditLogEntry, ControllerInfo, DbError, DelegatedAccountInfo, DelegationActionType, 5 + DelegationGrant, DelegationRepository, 6 + }; 7 + use tranquil_types::Did; 8 + use uuid::Uuid; 9 + 10 + use super::user::map_sqlx_error; 11 + 12 + #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)] 13 + #[sqlx(type_name = "delegation_action_type", rename_all = "snake_case")] 14 + pub enum PgDelegationActionType { 15 + GrantCreated, 16 + GrantRevoked, 17 + ScopesModified, 18 + TokenIssued, 19 + RepoWrite, 20 + BlobUpload, 21 + AccountAction, 22 + } 23 + 24 + impl From<DelegationActionType> for PgDelegationActionType { 25 + fn from(t: DelegationActionType) -> Self { 26 + match t { 27 + DelegationActionType::GrantCreated => Self::GrantCreated, 28 + DelegationActionType::GrantRevoked => Self::GrantRevoked, 29 + DelegationActionType::ScopesModified => Self::ScopesModified, 30 + DelegationActionType::TokenIssued => Self::TokenIssued, 31 + DelegationActionType::RepoWrite => Self::RepoWrite, 32 + DelegationActionType::BlobUpload => Self::BlobUpload, 33 + DelegationActionType::AccountAction => Self::AccountAction, 34 + } 35 + } 36 + } 37 + 38 + impl From<PgDelegationActionType> for DelegationActionType { 39 + fn from(t: PgDelegationActionType) -> Self { 40 + match t { 41 + PgDelegationActionType::GrantCreated => Self::GrantCreated, 42 + PgDelegationActionType::GrantRevoked => Self::GrantRevoked, 43 + PgDelegationActionType::ScopesModified => Self::ScopesModified, 44 + PgDelegationActionType::TokenIssued => Self::TokenIssued, 45 + PgDelegationActionType::RepoWrite => Self::RepoWrite, 46 + PgDelegationActionType::BlobUpload => Self::BlobUpload, 47 + PgDelegationActionType::AccountAction => Self::AccountAction, 48 + } 49 + } 50 + } 51 + 52 + pub struct PostgresDelegationRepository { 53 + pool: PgPool, 54 + } 55 + 56 + impl PostgresDelegationRepository { 57 + pub fn new(pool: PgPool) -> Self { 58 + Self { pool } 59 + } 60 + } 61 + 62 + #[async_trait] 63 + impl DelegationRepository for PostgresDelegationRepository { 64 + async fn is_delegated_account(&self, did: &Did) -> Result<bool, DbError> { 65 + let result = sqlx::query_scalar!( 66 + r#"SELECT account_type::text = 'delegated' as "is_delegated!" FROM users WHERE did = $1"#, 67 + did.as_str() 68 + ) 69 + .fetch_optional(&self.pool) 70 + .await 71 + .map_err(map_sqlx_error)?; 72 + 73 + Ok(result.unwrap_or(false)) 74 + } 75 + 76 + async fn create_delegation( 77 + &self, 78 + delegated_did: &Did, 79 + controller_did: &Did, 80 + granted_scopes: &str, 81 + granted_by: &Did, 82 + ) -> Result<Uuid, DbError> { 83 + let id = sqlx::query_scalar!( 84 + r#" 85 + INSERT INTO account_delegations (delegated_did, controller_did, granted_scopes, granted_by) 86 + VALUES ($1, $2, $3, $4) 87 + RETURNING id 88 + "#, 89 + delegated_did.as_str(), 90 + controller_did.as_str(), 91 + granted_scopes, 92 + granted_by.as_str() 93 + ) 94 + .fetch_one(&self.pool) 95 + .await 96 + .map_err(map_sqlx_error)?; 97 + 98 + Ok(id) 99 + } 100 + 101 + async fn revoke_delegation( 102 + &self, 103 + delegated_did: &Did, 104 + controller_did: &Did, 105 + revoked_by: &Did, 106 + ) -> Result<bool, DbError> { 107 + let result = sqlx::query!( 108 + r#" 109 + UPDATE account_delegations 110 + SET revoked_at = NOW(), revoked_by = $1 111 + WHERE delegated_did = $2 AND controller_did = $3 AND revoked_at IS NULL 112 + "#, 113 + revoked_by.as_str(), 114 + delegated_did.as_str(), 115 + controller_did.as_str() 116 + ) 117 + .execute(&self.pool) 118 + .await 119 + .map_err(map_sqlx_error)?; 120 + 121 + Ok(result.rows_affected() > 0) 122 + } 123 + 124 + async fn update_delegation_scopes( 125 + &self, 126 + delegated_did: &Did, 127 + controller_did: &Did, 128 + new_scopes: &str, 129 + ) -> Result<bool, DbError> { 130 + let result = sqlx::query!( 131 + r#" 132 + UPDATE account_delegations 133 + SET granted_scopes = $1 134 + WHERE delegated_did = $2 AND controller_did = $3 AND revoked_at IS NULL 135 + "#, 136 + new_scopes, 137 + delegated_did.as_str(), 138 + controller_did.as_str() 139 + ) 140 + .execute(&self.pool) 141 + .await 142 + .map_err(map_sqlx_error)?; 143 + 144 + Ok(result.rows_affected() > 0) 145 + } 146 + 147 + async fn get_delegation( 148 + &self, 149 + delegated_did: &Did, 150 + controller_did: &Did, 151 + ) -> Result<Option<DelegationGrant>, DbError> { 152 + let row = sqlx::query!( 153 + r#" 154 + SELECT id, delegated_did, controller_did, granted_scopes, 155 + granted_at, granted_by, revoked_at, revoked_by 156 + FROM account_delegations 157 + WHERE delegated_did = $1 AND controller_did = $2 AND revoked_at IS NULL 158 + "#, 159 + delegated_did.as_str(), 160 + controller_did.as_str() 161 + ) 162 + .fetch_optional(&self.pool) 163 + .await 164 + .map_err(map_sqlx_error)?; 165 + 166 + Ok(row.map(|r| DelegationGrant { 167 + id: r.id, 168 + delegated_did: r.delegated_did.into(), 169 + controller_did: r.controller_did.into(), 170 + granted_scopes: r.granted_scopes, 171 + granted_at: r.granted_at, 172 + granted_by: r.granted_by.into(), 173 + revoked_at: r.revoked_at, 174 + revoked_by: r.revoked_by.map(Into::into), 175 + })) 176 + } 177 + 178 + async fn get_delegations_for_account( 179 + &self, 180 + delegated_did: &Did, 181 + ) -> Result<Vec<ControllerInfo>, DbError> { 182 + let rows = sqlx::query!( 183 + r#" 184 + SELECT 185 + u.did, 186 + u.handle, 187 + d.granted_scopes, 188 + d.granted_at, 189 + (u.deactivated_at IS NULL AND u.takedown_ref IS NULL) as "is_active!" 190 + FROM account_delegations d 191 + JOIN users u ON u.did = d.controller_did 192 + WHERE d.delegated_did = $1 AND d.revoked_at IS NULL 193 + ORDER BY d.granted_at DESC 194 + "#, 195 + delegated_did.as_str() 196 + ) 197 + .fetch_all(&self.pool) 198 + .await 199 + .map_err(map_sqlx_error)?; 200 + 201 + Ok(rows 202 + .into_iter() 203 + .map(|r| ControllerInfo { 204 + did: r.did.into(), 205 + handle: r.handle.into(), 206 + granted_scopes: r.granted_scopes, 207 + granted_at: r.granted_at, 208 + is_active: r.is_active, 209 + }) 210 + .collect()) 211 + } 212 + 213 + async fn get_accounts_controlled_by( 214 + &self, 215 + controller_did: &Did, 216 + ) -> Result<Vec<DelegatedAccountInfo>, DbError> { 217 + let rows = sqlx::query!( 218 + r#" 219 + SELECT 220 + u.did, 221 + u.handle, 222 + d.granted_scopes, 223 + d.granted_at 224 + FROM account_delegations d 225 + JOIN users u ON u.did = d.delegated_did 226 + WHERE d.controller_did = $1 227 + AND d.revoked_at IS NULL 228 + AND u.deactivated_at IS NULL 229 + AND u.takedown_ref IS NULL 230 + ORDER BY d.granted_at DESC 231 + "#, 232 + controller_did.as_str() 233 + ) 234 + .fetch_all(&self.pool) 235 + .await 236 + .map_err(map_sqlx_error)?; 237 + 238 + Ok(rows 239 + .into_iter() 240 + .map(|r| DelegatedAccountInfo { 241 + did: r.did.into(), 242 + handle: r.handle.into(), 243 + granted_scopes: r.granted_scopes, 244 + granted_at: r.granted_at, 245 + }) 246 + .collect()) 247 + } 248 + 249 + async fn get_active_controllers_for_account( 250 + &self, 251 + delegated_did: &Did, 252 + ) -> Result<Vec<ControllerInfo>, DbError> { 253 + let rows = sqlx::query!( 254 + r#" 255 + SELECT 256 + u.did, 257 + u.handle, 258 + d.granted_scopes, 259 + d.granted_at, 260 + true as "is_active!" 261 + FROM account_delegations d 262 + JOIN users u ON u.did = d.controller_did 263 + WHERE d.delegated_did = $1 264 + AND d.revoked_at IS NULL 265 + AND u.deactivated_at IS NULL 266 + AND u.takedown_ref IS NULL 267 + ORDER BY d.granted_at DESC 268 + "#, 269 + delegated_did.as_str() 270 + ) 271 + .fetch_all(&self.pool) 272 + .await 273 + .map_err(map_sqlx_error)?; 274 + 275 + Ok(rows 276 + .into_iter() 277 + .map(|r| ControllerInfo { 278 + did: r.did.into(), 279 + handle: r.handle.into(), 280 + granted_scopes: r.granted_scopes, 281 + granted_at: r.granted_at, 282 + is_active: r.is_active, 283 + }) 284 + .collect()) 285 + } 286 + 287 + async fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, DbError> { 288 + let count = sqlx::query_scalar!( 289 + r#" 290 + SELECT COUNT(*) as "count!" 291 + FROM account_delegations d 292 + JOIN users u ON u.did = d.controller_did 293 + WHERE d.delegated_did = $1 294 + AND d.revoked_at IS NULL 295 + AND u.deactivated_at IS NULL 296 + AND u.takedown_ref IS NULL 297 + "#, 298 + delegated_did.as_str() 299 + ) 300 + .fetch_one(&self.pool) 301 + .await 302 + .map_err(map_sqlx_error)?; 303 + 304 + Ok(count) 305 + } 306 + 307 + async fn has_any_controllers(&self, did: &Did) -> Result<bool, DbError> { 308 + let exists = sqlx::query_scalar!( 309 + r#"SELECT EXISTS( 310 + SELECT 1 FROM account_delegations 311 + WHERE delegated_did = $1 AND revoked_at IS NULL 312 + ) as "exists!""#, 313 + did.as_str() 314 + ) 315 + .fetch_one(&self.pool) 316 + .await 317 + .map_err(map_sqlx_error)?; 318 + 319 + Ok(exists) 320 + } 321 + 322 + async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError> { 323 + let exists = sqlx::query_scalar!( 324 + r#"SELECT EXISTS( 325 + SELECT 1 FROM account_delegations 326 + WHERE controller_did = $1 AND revoked_at IS NULL 327 + ) as "exists!""#, 328 + did.as_str() 329 + ) 330 + .fetch_one(&self.pool) 331 + .await 332 + .map_err(map_sqlx_error)?; 333 + 334 + Ok(exists) 335 + } 336 + 337 + async fn log_delegation_action( 338 + &self, 339 + delegated_did: &Did, 340 + actor_did: &Did, 341 + controller_did: Option<&Did>, 342 + action_type: DelegationActionType, 343 + action_details: Option<serde_json::Value>, 344 + ip_address: Option<&str>, 345 + user_agent: Option<&str>, 346 + ) -> Result<Uuid, DbError> { 347 + let pg_action_type: PgDelegationActionType = action_type.into(); 348 + let controller_did_str = controller_did.map(|d| d.as_str()); 349 + let id = sqlx::query_scalar!( 350 + r#" 351 + INSERT INTO delegation_audit_log 352 + (delegated_did, actor_did, controller_did, action_type, action_details, ip_address, user_agent) 353 + VALUES ($1, $2, $3, $4, $5, $6, $7) 354 + RETURNING id 355 + "#, 356 + delegated_did.as_str(), 357 + actor_did.as_str(), 358 + controller_did_str, 359 + pg_action_type as PgDelegationActionType, 360 + action_details, 361 + ip_address, 362 + user_agent 363 + ) 364 + .fetch_one(&self.pool) 365 + .await 366 + .map_err(map_sqlx_error)?; 367 + 368 + Ok(id) 369 + } 370 + 371 + async fn get_audit_log_for_account( 372 + &self, 373 + delegated_did: &Did, 374 + limit: i64, 375 + offset: i64, 376 + ) -> Result<Vec<AuditLogEntry>, DbError> { 377 + let rows = sqlx::query!( 378 + r#" 379 + SELECT 380 + id, 381 + delegated_did, 382 + actor_did, 383 + controller_did, 384 + action_type as "action_type: PgDelegationActionType", 385 + action_details, 386 + ip_address, 387 + user_agent, 388 + created_at 389 + FROM delegation_audit_log 390 + WHERE delegated_did = $1 391 + ORDER BY created_at DESC 392 + LIMIT $2 OFFSET $3 393 + "#, 394 + delegated_did.as_str(), 395 + limit, 396 + offset 397 + ) 398 + .fetch_all(&self.pool) 399 + .await 400 + .map_err(map_sqlx_error)?; 401 + 402 + Ok(rows 403 + .into_iter() 404 + .map(|r| AuditLogEntry { 405 + id: r.id, 406 + delegated_did: r.delegated_did.into(), 407 + actor_did: r.actor_did.into(), 408 + controller_did: r.controller_did.map(Into::into), 409 + action_type: r.action_type.into(), 410 + action_details: r.action_details, 411 + ip_address: r.ip_address, 412 + user_agent: r.user_agent, 413 + created_at: r.created_at, 414 + }) 415 + .collect()) 416 + } 417 + 418 + async fn get_audit_log_by_controller( 419 + &self, 420 + controller_did: &Did, 421 + limit: i64, 422 + offset: i64, 423 + ) -> Result<Vec<AuditLogEntry>, DbError> { 424 + let rows = sqlx::query!( 425 + r#" 426 + SELECT 427 + id, 428 + delegated_did, 429 + actor_did, 430 + controller_did, 431 + action_type as "action_type: PgDelegationActionType", 432 + action_details, 433 + ip_address, 434 + user_agent, 435 + created_at 436 + FROM delegation_audit_log 437 + WHERE controller_did = $1 438 + ORDER BY created_at DESC 439 + LIMIT $2 OFFSET $3 440 + "#, 441 + controller_did.as_str(), 442 + limit, 443 + offset 444 + ) 445 + .fetch_all(&self.pool) 446 + .await 447 + .map_err(map_sqlx_error)?; 448 + 449 + Ok(rows 450 + .into_iter() 451 + .map(|r| AuditLogEntry { 452 + id: r.id, 453 + delegated_did: r.delegated_did.into(), 454 + actor_did: r.actor_did.into(), 455 + controller_did: r.controller_did.map(Into::into), 456 + action_type: r.action_type.into(), 457 + action_details: r.action_details, 458 + ip_address: r.ip_address, 459 + user_agent: r.user_agent, 460 + created_at: r.created_at, 461 + }) 462 + .collect()) 463 + } 464 + 465 + async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, DbError> { 466 + let count = sqlx::query_scalar!( 467 + r#"SELECT COUNT(*) as "count!" FROM delegation_audit_log WHERE delegated_did = $1"#, 468 + delegated_did.as_str() 469 + ) 470 + .fetch_one(&self.pool) 471 + .await 472 + .map_err(map_sqlx_error)?; 473 + 474 + Ok(count) 475 + } 476 + }
+41
crates/tranquil-db/src/postgres/event_notifier.rs
··· 1 + use async_trait::async_trait; 2 + use sqlx::postgres::PgListener; 3 + use sqlx::PgPool; 4 + use tranquil_db_traits::{DbError, RepoEventNotifier, RepoEventReceiver}; 5 + 6 + use super::user::map_sqlx_error; 7 + 8 + pub struct PostgresRepoEventNotifier { 9 + pool: PgPool, 10 + } 11 + 12 + impl PostgresRepoEventNotifier { 13 + pub fn new(pool: PgPool) -> Self { 14 + Self { pool } 15 + } 16 + } 17 + 18 + #[async_trait] 19 + impl RepoEventNotifier for PostgresRepoEventNotifier { 20 + async fn subscribe(&self) -> Result<Box<dyn RepoEventReceiver>, DbError> { 21 + let mut listener = PgListener::connect_with(&self.pool) 22 + .await 23 + .map_err(map_sqlx_error)?; 24 + listener.listen("repo_updates").await.map_err(map_sqlx_error)?; 25 + Ok(Box::new(PostgresRepoEventReceiver { listener })) 26 + } 27 + } 28 + 29 + pub struct PostgresRepoEventReceiver { 30 + listener: PgListener, 31 + } 32 + 33 + #[async_trait] 34 + impl RepoEventReceiver for PostgresRepoEventReceiver { 35 + async fn recv(&mut self) -> Option<i64> { 36 + match self.listener.recv().await { 37 + Ok(notification) => notification.payload().parse().ok(), 38 + Err(_) => None, 39 + } 40 + } 41 + }
+1018
crates/tranquil-db/src/postgres/infra.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use sqlx::PgPool; 4 + use tranquil_db_traits::{ 5 + AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DbError, DeletionRequest, 6 + InfraRepository, InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, InviteCodeUse, 7 + NotificationHistoryRow, QueuedComms, ReservedSigningKey, 8 + }; 9 + use tranquil_types::{CidLink, Did, Handle}; 10 + use uuid::Uuid; 11 + 12 + use super::user::map_sqlx_error; 13 + 14 + pub struct PostgresInfraRepository { 15 + pool: PgPool, 16 + } 17 + 18 + impl PostgresInfraRepository { 19 + pub fn new(pool: PgPool) -> Self { 20 + Self { pool } 21 + } 22 + } 23 + 24 + #[async_trait] 25 + impl InfraRepository for PostgresInfraRepository { 26 + async fn enqueue_comms( 27 + &self, 28 + user_id: Option<Uuid>, 29 + channel: CommsChannel, 30 + comms_type: CommsType, 31 + recipient: &str, 32 + subject: Option<&str>, 33 + body: &str, 34 + metadata: Option<serde_json::Value>, 35 + ) -> Result<Uuid, DbError> { 36 + let id = sqlx::query_scalar!( 37 + r#"INSERT INTO comms_queue 38 + (user_id, channel, comms_type, recipient, subject, body, metadata) 39 + VALUES ($1, $2, $3, $4, $5, $6, $7) 40 + RETURNING id"#, 41 + user_id, 42 + channel as CommsChannel, 43 + comms_type as CommsType, 44 + recipient, 45 + subject, 46 + body, 47 + metadata 48 + ) 49 + .fetch_one(&self.pool) 50 + .await 51 + .map_err(map_sqlx_error)?; 52 + 53 + Ok(id) 54 + } 55 + 56 + async fn fetch_pending_comms( 57 + &self, 58 + now: DateTime<Utc>, 59 + batch_size: i64, 60 + ) -> Result<Vec<QueuedComms>, DbError> { 61 + let results = sqlx::query_as!( 62 + QueuedComms, 63 + r#"UPDATE comms_queue 64 + SET status = 'processing', updated_at = NOW() 65 + WHERE id IN ( 66 + SELECT id FROM comms_queue 67 + WHERE status = 'pending' 68 + AND scheduled_for <= $1 69 + AND attempts < max_attempts 70 + ORDER BY scheduled_for ASC 71 + LIMIT $2 72 + FOR UPDATE SKIP LOCKED 73 + ) 74 + RETURNING 75 + id, user_id, 76 + channel as "channel: CommsChannel", 77 + comms_type as "comms_type: CommsType", 78 + status as "status: CommsStatus", 79 + recipient, subject, body, metadata, 80 + attempts, max_attempts, last_error, 81 + created_at, updated_at, scheduled_for, processed_at"#, 82 + now, 83 + batch_size 84 + ) 85 + .fetch_all(&self.pool) 86 + .await 87 + .map_err(map_sqlx_error)?; 88 + 89 + Ok(results) 90 + } 91 + 92 + async fn mark_comms_sent(&self, id: Uuid) -> Result<(), DbError> { 93 + sqlx::query!( 94 + r#"UPDATE comms_queue 95 + SET status = 'sent', processed_at = NOW(), updated_at = NOW() 96 + WHERE id = $1"#, 97 + id 98 + ) 99 + .execute(&self.pool) 100 + .await 101 + .map_err(map_sqlx_error)?; 102 + 103 + Ok(()) 104 + } 105 + 106 + async fn mark_comms_failed(&self, id: Uuid, error: &str) -> Result<(), DbError> { 107 + sqlx::query!( 108 + r#"UPDATE comms_queue 109 + SET 110 + status = CASE 111 + WHEN attempts + 1 >= max_attempts THEN 'failed'::comms_status 112 + ELSE 'pending'::comms_status 113 + END, 114 + attempts = attempts + 1, 115 + last_error = $2, 116 + updated_at = NOW(), 117 + scheduled_for = NOW() + (INTERVAL '1 minute' * (attempts + 1)) 118 + WHERE id = $1"#, 119 + id, 120 + error 121 + ) 122 + .execute(&self.pool) 123 + .await 124 + .map_err(map_sqlx_error)?; 125 + 126 + Ok(()) 127 + } 128 + 129 + async fn create_invite_code( 130 + &self, 131 + code: &str, 132 + use_count: i32, 133 + for_account: Option<&Did>, 134 + ) -> Result<bool, DbError> { 135 + let for_account_str = for_account.map(|d| d.as_str()); 136 + let result = sqlx::query!( 137 + r#"INSERT INTO invite_codes (code, available_uses, created_by_user, for_account) 138 + SELECT $1, $2, id, $3 FROM users WHERE is_admin = true LIMIT 1"#, 139 + code, 140 + use_count, 141 + for_account_str 142 + ) 143 + .execute(&self.pool) 144 + .await 145 + .map_err(map_sqlx_error)?; 146 + 147 + Ok(result.rows_affected() > 0) 148 + } 149 + 150 + async fn create_invite_codes_batch( 151 + &self, 152 + codes: &[String], 153 + use_count: i32, 154 + created_by_user: Uuid, 155 + for_account: Option<&Did>, 156 + ) -> Result<(), DbError> { 157 + let for_account_str = for_account.map(|d| d.as_str()); 158 + sqlx::query!( 159 + r#"INSERT INTO invite_codes (code, available_uses, created_by_user, for_account) 160 + SELECT code, $2, $3, $4 FROM UNNEST($1::text[]) AS t(code)"#, 161 + codes, 162 + use_count, 163 + created_by_user, 164 + for_account_str 165 + ) 166 + .execute(&self.pool) 167 + .await 168 + .map_err(map_sqlx_error)?; 169 + 170 + Ok(()) 171 + } 172 + 173 + async fn get_invite_code_available_uses(&self, code: &str) -> Result<Option<i32>, DbError> { 174 + let result = sqlx::query_scalar!( 175 + "SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE", 176 + code 177 + ) 178 + .fetch_optional(&self.pool) 179 + .await 180 + .map_err(map_sqlx_error)?; 181 + 182 + Ok(result) 183 + } 184 + 185 + async fn is_invite_code_valid(&self, code: &str) -> Result<bool, DbError> { 186 + let result = sqlx::query_scalar!( 187 + r#"SELECT (available_uses > 0 AND NOT COALESCE(disabled, false)) as "valid!" FROM invite_codes WHERE code = $1"#, 188 + code 189 + ) 190 + .fetch_optional(&self.pool) 191 + .await 192 + .map_err(map_sqlx_error)?; 193 + 194 + Ok(result.unwrap_or(false)) 195 + } 196 + 197 + async fn decrement_invite_code_uses(&self, code: &str) -> Result<(), DbError> { 198 + sqlx::query!( 199 + "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 200 + code 201 + ) 202 + .execute(&self.pool) 203 + .await 204 + .map_err(map_sqlx_error)?; 205 + 206 + Ok(()) 207 + } 208 + 209 + async fn record_invite_code_use(&self, code: &str, used_by_user: Uuid) -> Result<(), DbError> { 210 + sqlx::query!( 211 + "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", 212 + code, 213 + used_by_user 214 + ) 215 + .execute(&self.pool) 216 + .await 217 + .map_err(map_sqlx_error)?; 218 + 219 + Ok(()) 220 + } 221 + 222 + async fn get_invite_codes_for_account( 223 + &self, 224 + for_account: &Did, 225 + ) -> Result<Vec<InviteCodeInfo>, DbError> { 226 + let results = sqlx::query!( 227 + r#"SELECT 228 + ic.code, 229 + ic.available_uses, 230 + ic.created_at, 231 + ic.disabled, 232 + ic.for_account, 233 + (SELECT COUNT(*) FROM invite_code_uses icu WHERE icu.code = ic.code)::int as "use_count!" 234 + FROM invite_codes ic 235 + WHERE ic.for_account = $1 236 + ORDER BY ic.created_at DESC"#, 237 + for_account.as_str() 238 + ) 239 + .fetch_all(&self.pool) 240 + .await 241 + .map_err(map_sqlx_error)?; 242 + 243 + Ok(results 244 + .into_iter() 245 + .map(|r| InviteCodeInfo { 246 + code: r.code, 247 + available_uses: r.available_uses, 248 + disabled: r.disabled.unwrap_or(false), 249 + for_account: Some(Did::from(r.for_account)), 250 + created_at: r.created_at, 251 + created_by: None, 252 + }) 253 + .collect()) 254 + } 255 + 256 + async fn get_invite_code_uses(&self, code: &str) -> Result<Vec<InviteCodeUse>, DbError> { 257 + let results = sqlx::query!( 258 + r#"SELECT u.did, u.handle, icu.used_at 259 + FROM invite_code_uses icu 260 + JOIN users u ON icu.used_by_user = u.id 261 + WHERE icu.code = $1 262 + ORDER BY icu.used_at DESC"#, 263 + code 264 + ) 265 + .fetch_all(&self.pool) 266 + .await 267 + .map_err(map_sqlx_error)?; 268 + 269 + Ok(results 270 + .into_iter() 271 + .map(|r| InviteCodeUse { 272 + code: code.to_string(), 273 + used_by_did: Did::from(r.did), 274 + used_by_handle: Some(Handle::from(r.handle)), 275 + used_at: r.used_at, 276 + }) 277 + .collect()) 278 + } 279 + 280 + async fn disable_invite_codes_by_code(&self, codes: &[String]) -> Result<(), DbError> { 281 + sqlx::query!( 282 + "UPDATE invite_codes SET disabled = TRUE WHERE code = ANY($1)", 283 + codes 284 + ) 285 + .execute(&self.pool) 286 + .await 287 + .map_err(map_sqlx_error)?; 288 + 289 + Ok(()) 290 + } 291 + 292 + async fn disable_invite_codes_by_account(&self, accounts: &[Did]) -> Result<(), DbError> { 293 + let accounts_str: Vec<&str> = accounts.iter().map(|d| d.as_str()).collect(); 294 + sqlx::query!( 295 + r#"UPDATE invite_codes SET disabled = TRUE 296 + WHERE created_by_user IN (SELECT id FROM users WHERE did = ANY($1))"#, 297 + &accounts_str as &[&str] 298 + ) 299 + .execute(&self.pool) 300 + .await 301 + .map_err(map_sqlx_error)?; 302 + 303 + Ok(()) 304 + } 305 + 306 + async fn list_invite_codes( 307 + &self, 308 + cursor: Option<&str>, 309 + limit: i64, 310 + sort: InviteCodeSortOrder, 311 + ) -> Result<Vec<InviteCodeRow>, DbError> { 312 + let results = match (cursor, sort) { 313 + (Some(cursor_code), InviteCodeSortOrder::Recent) => { 314 + sqlx::query_as!( 315 + InviteCodeRow, 316 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 317 + FROM invite_codes ic 318 + WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 319 + ORDER BY created_at DESC 320 + LIMIT $2"#, 321 + cursor_code, 322 + limit 323 + ) 324 + .fetch_all(&self.pool) 325 + .await 326 + .map_err(map_sqlx_error)? 327 + } 328 + (None, InviteCodeSortOrder::Recent) => { 329 + sqlx::query_as!( 330 + InviteCodeRow, 331 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 332 + FROM invite_codes ic 333 + ORDER BY created_at DESC 334 + LIMIT $1"#, 335 + limit 336 + ) 337 + .fetch_all(&self.pool) 338 + .await 339 + .map_err(map_sqlx_error)? 340 + } 341 + (Some(cursor_code), InviteCodeSortOrder::Usage) => { 342 + sqlx::query_as!( 343 + InviteCodeRow, 344 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 345 + FROM invite_codes ic 346 + WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 347 + ORDER BY available_uses DESC 348 + LIMIT $2"#, 349 + cursor_code, 350 + limit 351 + ) 352 + .fetch_all(&self.pool) 353 + .await 354 + .map_err(map_sqlx_error)? 355 + } 356 + (None, InviteCodeSortOrder::Usage) => { 357 + sqlx::query_as!( 358 + InviteCodeRow, 359 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 360 + FROM invite_codes ic 361 + ORDER BY available_uses DESC 362 + LIMIT $1"#, 363 + limit 364 + ) 365 + .fetch_all(&self.pool) 366 + .await 367 + .map_err(map_sqlx_error)? 368 + } 369 + }; 370 + 371 + Ok(results) 372 + } 373 + 374 + async fn get_user_dids_by_ids(&self, user_ids: &[Uuid]) -> Result<Vec<(Uuid, Did)>, DbError> { 375 + let results = sqlx::query!( 376 + "SELECT id, did FROM users WHERE id = ANY($1)", 377 + user_ids 378 + ) 379 + .fetch_all(&self.pool) 380 + .await 381 + .map_err(map_sqlx_error)?; 382 + 383 + Ok(results.into_iter().map(|r| (r.id, Did::from(r.did))).collect()) 384 + } 385 + 386 + async fn get_invite_code_uses_batch( 387 + &self, 388 + codes: &[String], 389 + ) -> Result<Vec<InviteCodeUse>, DbError> { 390 + let results = sqlx::query!( 391 + r#"SELECT icu.code, u.did, icu.used_at 392 + FROM invite_code_uses icu 393 + JOIN users u ON icu.used_by_user = u.id 394 + WHERE icu.code = ANY($1) 395 + ORDER BY icu.used_at DESC"#, 396 + codes 397 + ) 398 + .fetch_all(&self.pool) 399 + .await 400 + .map_err(map_sqlx_error)?; 401 + 402 + Ok(results 403 + .into_iter() 404 + .map(|r| InviteCodeUse { 405 + code: r.code, 406 + used_by_did: Did::from(r.did), 407 + used_by_handle: None, 408 + used_at: r.used_at, 409 + }) 410 + .collect()) 411 + } 412 + 413 + async fn get_invites_created_by_user( 414 + &self, 415 + user_id: Uuid, 416 + ) -> Result<Vec<InviteCodeInfo>, DbError> { 417 + let results = sqlx::query!( 418 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by 419 + FROM invite_codes ic 420 + JOIN users u ON ic.created_by_user = u.id 421 + WHERE ic.created_by_user = $1"#, 422 + user_id 423 + ) 424 + .fetch_all(&self.pool) 425 + .await 426 + .map_err(map_sqlx_error)?; 427 + 428 + Ok(results 429 + .into_iter() 430 + .map(|r| InviteCodeInfo { 431 + code: r.code, 432 + available_uses: r.available_uses, 433 + disabled: r.disabled.unwrap_or(false), 434 + for_account: Some(Did::from(r.for_account)), 435 + created_at: r.created_at, 436 + created_by: Some(Did::from(r.created_by)), 437 + }) 438 + .collect()) 439 + } 440 + 441 + async fn get_invite_code_info(&self, code: &str) -> Result<Option<InviteCodeInfo>, DbError> { 442 + let result = sqlx::query!( 443 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by 444 + FROM invite_codes ic 445 + JOIN users u ON ic.created_by_user = u.id 446 + WHERE ic.code = $1"#, 447 + code 448 + ) 449 + .fetch_optional(&self.pool) 450 + .await 451 + .map_err(map_sqlx_error)?; 452 + 453 + Ok(result.map(|r| InviteCodeInfo { 454 + code: r.code, 455 + available_uses: r.available_uses, 456 + disabled: r.disabled.unwrap_or(false), 457 + for_account: Some(Did::from(r.for_account)), 458 + created_at: r.created_at, 459 + created_by: Some(Did::from(r.created_by)), 460 + })) 461 + } 462 + 463 + async fn get_invite_codes_by_users( 464 + &self, 465 + user_ids: &[Uuid], 466 + ) -> Result<Vec<(Uuid, InviteCodeInfo)>, DbError> { 467 + let results = sqlx::query!( 468 + r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, 469 + ic.created_by_user, u.did as created_by 470 + FROM invite_codes ic 471 + JOIN users u ON ic.created_by_user = u.id 472 + WHERE ic.created_by_user = ANY($1)"#, 473 + user_ids 474 + ) 475 + .fetch_all(&self.pool) 476 + .await 477 + .map_err(map_sqlx_error)?; 478 + 479 + Ok(results 480 + .into_iter() 481 + .map(|r| { 482 + ( 483 + r.created_by_user, 484 + InviteCodeInfo { 485 + code: r.code, 486 + available_uses: r.available_uses, 487 + disabled: r.disabled.unwrap_or(false), 488 + for_account: Some(Did::from(r.for_account)), 489 + created_at: r.created_at, 490 + created_by: Some(Did::from(r.created_by)), 491 + }, 492 + ) 493 + }) 494 + .collect()) 495 + } 496 + 497 + async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result<Option<String>, DbError> { 498 + let result = sqlx::query_scalar!( 499 + "SELECT code FROM invite_code_uses WHERE used_by_user = $1", 500 + user_id 501 + ) 502 + .fetch_optional(&self.pool) 503 + .await 504 + .map_err(map_sqlx_error)?; 505 + 506 + Ok(result) 507 + } 508 + 509 + async fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), DbError> { 510 + sqlx::query!( 511 + "DELETE FROM invite_code_uses WHERE used_by_user = $1", 512 + user_id 513 + ) 514 + .execute(&self.pool) 515 + .await 516 + .map_err(map_sqlx_error)?; 517 + 518 + Ok(()) 519 + } 520 + 521 + async fn delete_invite_codes_by_user(&self, user_id: Uuid) -> Result<(), DbError> { 522 + sqlx::query!( 523 + "DELETE FROM invite_codes WHERE created_by_user = $1", 524 + user_id 525 + ) 526 + .execute(&self.pool) 527 + .await 528 + .map_err(map_sqlx_error)?; 529 + 530 + Ok(()) 531 + } 532 + 533 + async fn reserve_signing_key( 534 + &self, 535 + did: Option<&Did>, 536 + public_key_did_key: &str, 537 + private_key_bytes: &[u8], 538 + expires_at: DateTime<Utc>, 539 + ) -> Result<Uuid, DbError> { 540 + let did_str = did.map(|d| d.as_str()); 541 + let id = sqlx::query_scalar!( 542 + r#"INSERT INTO reserved_signing_keys (did, public_key_did_key, private_key_bytes, expires_at) 543 + VALUES ($1, $2, $3, $4) 544 + RETURNING id"#, 545 + did_str, 546 + public_key_did_key, 547 + private_key_bytes, 548 + expires_at 549 + ) 550 + .fetch_one(&self.pool) 551 + .await 552 + .map_err(map_sqlx_error)?; 553 + 554 + Ok(id) 555 + } 556 + 557 + async fn get_reserved_signing_key( 558 + &self, 559 + public_key_did_key: &str, 560 + ) -> Result<Option<ReservedSigningKey>, DbError> { 561 + let result = sqlx::query!( 562 + r#"SELECT id, private_key_bytes 563 + FROM reserved_signing_keys 564 + WHERE public_key_did_key = $1 565 + AND used_at IS NULL 566 + AND expires_at > NOW() 567 + FOR UPDATE"#, 568 + public_key_did_key 569 + ) 570 + .fetch_optional(&self.pool) 571 + .await 572 + .map_err(map_sqlx_error)?; 573 + 574 + Ok(result.map(|r| ReservedSigningKey { 575 + id: r.id, 576 + private_key_bytes: r.private_key_bytes, 577 + })) 578 + } 579 + 580 + async fn mark_signing_key_used(&self, key_id: Uuid) -> Result<(), DbError> { 581 + sqlx::query!( 582 + "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1", 583 + key_id 584 + ) 585 + .execute(&self.pool) 586 + .await 587 + .map_err(map_sqlx_error)?; 588 + 589 + Ok(()) 590 + } 591 + 592 + async fn create_deletion_request( 593 + &self, 594 + token: &str, 595 + did: &Did, 596 + expires_at: DateTime<Utc>, 597 + ) -> Result<(), DbError> { 598 + sqlx::query!( 599 + "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)", 600 + token, 601 + did.as_str(), 602 + expires_at 603 + ) 604 + .execute(&self.pool) 605 + .await 606 + .map_err(map_sqlx_error)?; 607 + 608 + Ok(()) 609 + } 610 + 611 + async fn get_deletion_request(&self, token: &str) -> Result<Option<DeletionRequest>, DbError> { 612 + let result = sqlx::query!( 613 + "SELECT did, expires_at FROM account_deletion_requests WHERE token = $1", 614 + token 615 + ) 616 + .fetch_optional(&self.pool) 617 + .await 618 + .map_err(map_sqlx_error)?; 619 + 620 + Ok(result.map(|r| DeletionRequest { 621 + did: Did::from(r.did), 622 + expires_at: r.expires_at, 623 + })) 624 + } 625 + 626 + async fn delete_deletion_request(&self, token: &str) -> Result<(), DbError> { 627 + sqlx::query!( 628 + "DELETE FROM account_deletion_requests WHERE token = $1", 629 + token 630 + ) 631 + .execute(&self.pool) 632 + .await 633 + .map_err(map_sqlx_error)?; 634 + 635 + Ok(()) 636 + } 637 + 638 + async fn delete_deletion_requests_by_did(&self, did: &Did) -> Result<(), DbError> { 639 + sqlx::query!( 640 + "DELETE FROM account_deletion_requests WHERE did = $1", 641 + did.as_str() 642 + ) 643 + .execute(&self.pool) 644 + .await 645 + .map_err(map_sqlx_error)?; 646 + 647 + Ok(()) 648 + } 649 + 650 + async fn upsert_account_preference( 651 + &self, 652 + user_id: Uuid, 653 + name: &str, 654 + value_json: serde_json::Value, 655 + ) -> Result<(), DbError> { 656 + sqlx::query!( 657 + r#"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 658 + ON CONFLICT (user_id, name) DO UPDATE SET value_json = $3"#, 659 + user_id, 660 + name, 661 + value_json 662 + ) 663 + .execute(&self.pool) 664 + .await 665 + .map_err(map_sqlx_error)?; 666 + 667 + Ok(()) 668 + } 669 + 670 + async fn insert_account_preference_if_not_exists( 671 + &self, 672 + user_id: Uuid, 673 + name: &str, 674 + value_json: serde_json::Value, 675 + ) -> Result<(), DbError> { 676 + sqlx::query!( 677 + r#"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 678 + ON CONFLICT (user_id, name) DO NOTHING"#, 679 + user_id, 680 + name, 681 + value_json 682 + ) 683 + .execute(&self.pool) 684 + .await 685 + .map_err(map_sqlx_error)?; 686 + 687 + Ok(()) 688 + } 689 + 690 + async fn get_server_config(&self, key: &str) -> Result<Option<String>, DbError> { 691 + let row = 692 + sqlx::query_scalar!("SELECT value FROM server_config WHERE key = $1", key) 693 + .fetch_optional(&self.pool) 694 + .await 695 + .map_err(map_sqlx_error)?; 696 + Ok(row) 697 + } 698 + 699 + async fn health_check(&self) -> Result<bool, DbError> { 700 + sqlx::query_scalar!("SELECT 1 as one") 701 + .fetch_one(&self.pool) 702 + .await 703 + .map_err(map_sqlx_error)?; 704 + Ok(true) 705 + } 706 + 707 + async fn insert_report( 708 + &self, 709 + id: i64, 710 + reason_type: &str, 711 + reason: Option<&str>, 712 + subject_json: serde_json::Value, 713 + reported_by_did: &Did, 714 + created_at: DateTime<Utc>, 715 + ) -> Result<(), DbError> { 716 + sqlx::query!( 717 + "INSERT INTO reports (id, reason_type, reason, subject_json, reported_by_did, created_at) VALUES ($1, $2, $3, $4, $5, $6)", 718 + id, 719 + reason_type, 720 + reason, 721 + subject_json, 722 + reported_by_did.as_str(), 723 + created_at 724 + ) 725 + .execute(&self.pool) 726 + .await 727 + .map_err(map_sqlx_error)?; 728 + 729 + Ok(()) 730 + } 731 + 732 + async fn delete_plc_tokens_for_user(&self, user_id: Uuid) -> Result<(), DbError> { 733 + sqlx::query!( 734 + "DELETE FROM plc_operation_tokens WHERE user_id = $1 OR expires_at < NOW()", 735 + user_id 736 + ) 737 + .execute(&self.pool) 738 + .await 739 + .map_err(map_sqlx_error)?; 740 + 741 + Ok(()) 742 + } 743 + 744 + async fn insert_plc_token( 745 + &self, 746 + user_id: Uuid, 747 + token: &str, 748 + expires_at: DateTime<Utc>, 749 + ) -> Result<(), DbError> { 750 + sqlx::query!( 751 + "INSERT INTO plc_operation_tokens (user_id, token, expires_at) VALUES ($1, $2, $3)", 752 + user_id, 753 + token, 754 + expires_at 755 + ) 756 + .execute(&self.pool) 757 + .await 758 + .map_err(map_sqlx_error)?; 759 + 760 + Ok(()) 761 + } 762 + 763 + async fn get_plc_token_expiry( 764 + &self, 765 + user_id: Uuid, 766 + token: &str, 767 + ) -> Result<Option<DateTime<Utc>>, DbError> { 768 + let expiry = sqlx::query_scalar!( 769 + "SELECT expires_at FROM plc_operation_tokens WHERE user_id = $1 AND token = $2", 770 + user_id, 771 + token 772 + ) 773 + .fetch_optional(&self.pool) 774 + .await 775 + .map_err(map_sqlx_error)?; 776 + 777 + Ok(expiry) 778 + } 779 + 780 + async fn delete_plc_token(&self, user_id: Uuid, token: &str) -> Result<(), DbError> { 781 + sqlx::query!( 782 + "DELETE FROM plc_operation_tokens WHERE user_id = $1 AND token = $2", 783 + user_id, 784 + token 785 + ) 786 + .execute(&self.pool) 787 + .await 788 + .map_err(map_sqlx_error)?; 789 + 790 + Ok(()) 791 + } 792 + 793 + async fn get_account_preferences( 794 + &self, 795 + user_id: Uuid, 796 + ) -> Result<Vec<(String, serde_json::Value)>, DbError> { 797 + let rows = sqlx::query!( 798 + "SELECT name, value_json FROM account_preferences WHERE user_id = $1", 799 + user_id 800 + ) 801 + .fetch_all(&self.pool) 802 + .await 803 + .map_err(map_sqlx_error)?; 804 + 805 + Ok(rows.into_iter().map(|r| (r.name, r.value_json)).collect()) 806 + } 807 + 808 + async fn replace_namespace_preferences( 809 + &self, 810 + user_id: Uuid, 811 + namespace: &str, 812 + preferences: Vec<(String, serde_json::Value)>, 813 + ) -> Result<(), DbError> { 814 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 815 + 816 + let like_pattern = format!("{}.%", namespace); 817 + sqlx::query!( 818 + "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)", 819 + user_id, 820 + namespace, 821 + like_pattern 822 + ) 823 + .execute(&mut *tx) 824 + .await 825 + .map_err(map_sqlx_error)?; 826 + 827 + for (name, value_json) in preferences { 828 + sqlx::query!( 829 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 830 + user_id, 831 + name, 832 + value_json 833 + ) 834 + .execute(&mut *tx) 835 + .await 836 + .map_err(map_sqlx_error)?; 837 + } 838 + 839 + tx.commit().await.map_err(map_sqlx_error)?; 840 + 841 + Ok(()) 842 + } 843 + 844 + async fn get_notification_history( 845 + &self, 846 + user_id: Uuid, 847 + limit: i64, 848 + ) -> Result<Vec<NotificationHistoryRow>, DbError> { 849 + let rows = sqlx::query!( 850 + r#" 851 + SELECT 852 + created_at, 853 + channel as "channel: String", 854 + comms_type as "comms_type: String", 855 + status as "status: String", 856 + subject, 857 + body 858 + FROM comms_queue 859 + WHERE user_id = $1 860 + ORDER BY created_at DESC 861 + LIMIT $2 862 + "#, 863 + user_id, 864 + limit 865 + ) 866 + .fetch_all(&self.pool) 867 + .await 868 + .map_err(map_sqlx_error)?; 869 + Ok(rows 870 + .into_iter() 871 + .map(|r| NotificationHistoryRow { 872 + created_at: r.created_at, 873 + channel: r.channel, 874 + comms_type: r.comms_type, 875 + status: r.status, 876 + subject: r.subject, 877 + body: r.body, 878 + }) 879 + .collect()) 880 + } 881 + 882 + async fn get_server_configs(&self, keys: &[&str]) -> Result<Vec<(String, String)>, DbError> { 883 + let keys_vec: Vec<String> = keys.iter().map(|s| s.to_string()).collect(); 884 + let rows: Vec<(String, String)> = sqlx::query_as( 885 + "SELECT key, value FROM server_config WHERE key = ANY($1)", 886 + ) 887 + .bind(&keys_vec) 888 + .fetch_all(&self.pool) 889 + .await 890 + .map_err(map_sqlx_error)?; 891 + 892 + Ok(rows) 893 + } 894 + 895 + async fn upsert_server_config(&self, key: &str, value: &str) -> Result<(), DbError> { 896 + sqlx::query( 897 + "INSERT INTO server_config (key, value, updated_at) VALUES ($1, $2, NOW()) 898 + ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()", 899 + ) 900 + .bind(key) 901 + .bind(value) 902 + .execute(&self.pool) 903 + .await 904 + .map_err(map_sqlx_error)?; 905 + 906 + Ok(()) 907 + } 908 + 909 + async fn delete_server_config(&self, key: &str) -> Result<(), DbError> { 910 + sqlx::query("DELETE FROM server_config WHERE key = $1") 911 + .bind(key) 912 + .execute(&self.pool) 913 + .await 914 + .map_err(map_sqlx_error)?; 915 + 916 + Ok(()) 917 + } 918 + 919 + async fn get_blob_storage_key_by_cid(&self, cid: &CidLink) -> Result<Option<String>, DbError> { 920 + let result = sqlx::query_scalar!("SELECT storage_key FROM blobs WHERE cid = $1", cid.as_str()) 921 + .fetch_optional(&self.pool) 922 + .await 923 + .map_err(map_sqlx_error)?; 924 + 925 + Ok(result) 926 + } 927 + 928 + async fn delete_blob_by_cid(&self, cid: &CidLink) -> Result<(), DbError> { 929 + sqlx::query!("DELETE FROM blobs WHERE cid = $1", cid.as_str()) 930 + .execute(&self.pool) 931 + .await 932 + .map_err(map_sqlx_error)?; 933 + 934 + Ok(()) 935 + } 936 + 937 + async fn get_admin_account_info_by_did( 938 + &self, 939 + did: &Did, 940 + ) -> Result<Option<AdminAccountInfo>, DbError> { 941 + let result = sqlx::query!( 942 + r#" 943 + SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at 944 + FROM users 945 + WHERE did = $1 946 + "#, 947 + did.as_str() 948 + ) 949 + .fetch_optional(&self.pool) 950 + .await 951 + .map_err(map_sqlx_error)?; 952 + 953 + Ok(result.map(|r| AdminAccountInfo { 954 + id: r.id, 955 + did: Did::from(r.did), 956 + handle: Handle::from(r.handle), 957 + email: r.email, 958 + created_at: r.created_at, 959 + invites_disabled: r.invites_disabled.unwrap_or(false), 960 + email_verified: r.email_verified, 961 + deactivated_at: r.deactivated_at, 962 + })) 963 + } 964 + 965 + async fn get_admin_account_infos_by_dids( 966 + &self, 967 + dids: &[Did], 968 + ) -> Result<Vec<AdminAccountInfo>, DbError> { 969 + let dids_str: Vec<&str> = dids.iter().map(|d| d.as_str()).collect(); 970 + let results = sqlx::query!( 971 + r#" 972 + SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at 973 + FROM users 974 + WHERE did = ANY($1) 975 + "#, 976 + &dids_str as &[&str] 977 + ) 978 + .fetch_all(&self.pool) 979 + .await 980 + .map_err(map_sqlx_error)?; 981 + 982 + Ok(results 983 + .into_iter() 984 + .map(|r| AdminAccountInfo { 985 + id: r.id, 986 + did: Did::from(r.did), 987 + handle: Handle::from(r.handle), 988 + email: r.email, 989 + created_at: r.created_at, 990 + invites_disabled: r.invites_disabled.unwrap_or(false), 991 + email_verified: r.email_verified, 992 + deactivated_at: r.deactivated_at, 993 + }) 994 + .collect()) 995 + } 996 + 997 + async fn get_invite_code_uses_by_users( 998 + &self, 999 + user_ids: &[Uuid], 1000 + ) -> Result<Vec<(Uuid, String)>, DbError> { 1001 + let results = sqlx::query!( 1002 + r#" 1003 + SELECT used_by_user, code 1004 + FROM invite_code_uses 1005 + WHERE used_by_user = ANY($1) 1006 + "#, 1007 + user_ids 1008 + ) 1009 + .fetch_all(&self.pool) 1010 + .await 1011 + .map_err(map_sqlx_error)?; 1012 + 1013 + Ok(results 1014 + .into_iter() 1015 + .map(|r| (r.used_by_user, r.code)) 1016 + .collect()) 1017 + } 1018 + }
+60
crates/tranquil-db/src/postgres/mod.rs
··· 1 + mod backlink; 2 + mod backup; 3 + mod blob; 4 + mod delegation; 5 + mod event_notifier; 6 + mod infra; 7 + mod oauth; 8 + mod repo; 9 + mod session; 10 + mod user; 11 + 12 + use sqlx::PgPool; 13 + use std::sync::Arc; 14 + 15 + pub use backlink::PostgresBacklinkRepository; 16 + pub use backup::PostgresBackupRepository; 17 + pub use blob::PostgresBlobRepository; 18 + pub use delegation::PostgresDelegationRepository; 19 + pub use event_notifier::PostgresRepoEventNotifier; 20 + pub use infra::PostgresInfraRepository; 21 + pub use oauth::PostgresOAuthRepository; 22 + pub use repo::PostgresRepoRepository; 23 + pub use session::PostgresSessionRepository; 24 + pub use user::PostgresUserRepository; 25 + use tranquil_db_traits::{ 26 + BacklinkRepository, BackupRepository, BlobRepository, DelegationRepository, InfraRepository, 27 + OAuthRepository, RepoEventNotifier, RepoRepository, SessionRepository, UserRepository, 28 + }; 29 + 30 + pub struct PostgresRepositories { 31 + pub pool: PgPool, 32 + pub user: Arc<dyn UserRepository>, 33 + pub oauth: Arc<dyn OAuthRepository>, 34 + pub session: Arc<dyn SessionRepository>, 35 + pub delegation: Arc<dyn DelegationRepository>, 36 + pub repo: Arc<dyn RepoRepository>, 37 + pub blob: Arc<dyn BlobRepository>, 38 + pub infra: Arc<dyn InfraRepository>, 39 + pub backup: Arc<dyn BackupRepository>, 40 + pub backlink: Arc<dyn BacklinkRepository>, 41 + pub event_notifier: Arc<dyn RepoEventNotifier>, 42 + } 43 + 44 + impl PostgresRepositories { 45 + pub fn new(pool: PgPool) -> Self { 46 + Self { 47 + pool: pool.clone(), 48 + user: Arc::new(PostgresUserRepository::new(pool.clone())), 49 + oauth: Arc::new(PostgresOAuthRepository::new(pool.clone())), 50 + session: Arc::new(PostgresSessionRepository::new(pool.clone())), 51 + delegation: Arc::new(PostgresDelegationRepository::new(pool.clone())), 52 + repo: Arc::new(PostgresRepoRepository::new(pool.clone())), 53 + blob: Arc::new(PostgresBlobRepository::new(pool.clone())), 54 + infra: Arc::new(PostgresInfraRepository::new(pool.clone())), 55 + backup: Arc::new(PostgresBackupRepository::new(pool.clone())), 56 + backlink: Arc::new(PostgresBacklinkRepository::new(pool.clone())), 57 + event_notifier: Arc::new(PostgresRepoEventNotifier::new(pool)), 58 + } 59 + } 60 + }
+1214
crates/tranquil-db/src/postgres/oauth.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Duration, Utc}; 3 + use rand::Rng; 4 + use sqlx::PgPool; 5 + use tranquil_db_traits::{ 6 + DbError, DeviceAccountRow, DeviceTrustInfo, OAuthRepository, OAuthSessionListItem, 7 + ScopePreference, TrustedDeviceRow, TwoFactorChallenge, 8 + }; 9 + use tranquil_oauth::{AuthorizedClientData, ClientAuth, AuthorizationRequestParameters, DeviceData, RequestData, TokenData}; 10 + use tranquil_types::{AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, TokenId}; 11 + use uuid::Uuid; 12 + 13 + use super::user::map_sqlx_error; 14 + 15 + fn to_json<T: serde::Serialize>(value: &T) -> Result<serde_json::Value, DbError> { 16 + serde_json::to_value(value).map_err(|e| { 17 + tracing::error!("JSON serialization error: {}", e); 18 + DbError::Serialization("Internal serialization error".to_string()) 19 + }) 20 + } 21 + 22 + fn from_json<T: serde::de::DeserializeOwned>(value: serde_json::Value) -> Result<T, DbError> { 23 + serde_json::from_value(value).map_err(|e| { 24 + tracing::error!("JSON deserialization error: {}", e); 25 + DbError::Serialization("Internal data corruption".to_string()) 26 + }) 27 + } 28 + 29 + pub struct PostgresOAuthRepository { 30 + pool: PgPool, 31 + } 32 + 33 + impl PostgresOAuthRepository { 34 + pub fn new(pool: PgPool) -> Self { 35 + Self { pool } 36 + } 37 + } 38 + 39 + const REFRESH_GRACE_PERIOD_SECS: i64 = 60; 40 + 41 + #[async_trait] 42 + impl OAuthRepository for PostgresOAuthRepository { 43 + async fn create_token(&self, data: &TokenData) -> Result<i32, DbError> { 44 + let client_auth_json = to_json(&data.client_auth)?; 45 + let parameters_json = to_json(&data.parameters)?; 46 + let row = sqlx::query!( 47 + r#" 48 + INSERT INTO oauth_token 49 + (did, token_id, created_at, updated_at, expires_at, client_id, client_auth, 50 + device_id, parameters, details, code, current_refresh_token, scope, controller_did) 51 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) 52 + RETURNING id 53 + "#, 54 + data.did, 55 + data.token_id, 56 + data.created_at, 57 + data.updated_at, 58 + data.expires_at, 59 + data.client_id, 60 + client_auth_json, 61 + data.device_id, 62 + parameters_json, 63 + data.details, 64 + data.code, 65 + data.current_refresh_token, 66 + data.scope, 67 + data.controller_did, 68 + ) 69 + .fetch_one(&self.pool) 70 + .await 71 + .map_err(map_sqlx_error)?; 72 + Ok(row.id) 73 + } 74 + 75 + async fn get_token_by_id(&self, token_id: &TokenId) -> Result<Option<TokenData>, DbError> { 76 + let row = sqlx::query!( 77 + r#" 78 + SELECT did, token_id, created_at, updated_at, expires_at, client_id, client_auth, 79 + device_id, parameters, details, code, current_refresh_token, scope, controller_did 80 + FROM oauth_token 81 + WHERE token_id = $1 82 + "#, 83 + token_id.as_str() 84 + ) 85 + .fetch_optional(&self.pool) 86 + .await 87 + .map_err(map_sqlx_error)?; 88 + match row { 89 + Some(r) => Ok(Some(TokenData { 90 + did: r.did, 91 + token_id: r.token_id, 92 + created_at: r.created_at, 93 + updated_at: r.updated_at, 94 + expires_at: r.expires_at, 95 + client_id: r.client_id, 96 + client_auth: from_json(r.client_auth)?, 97 + device_id: r.device_id, 98 + parameters: from_json(r.parameters)?, 99 + details: r.details, 100 + code: r.code, 101 + current_refresh_token: r.current_refresh_token, 102 + scope: r.scope, 103 + controller_did: r.controller_did, 104 + })), 105 + None => Ok(None), 106 + } 107 + } 108 + 109 + async fn get_token_by_refresh_token( 110 + &self, 111 + refresh_token: &RefreshToken, 112 + ) -> Result<Option<(i32, TokenData)>, DbError> { 113 + let row = sqlx::query!( 114 + r#" 115 + SELECT id, did, token_id, created_at, updated_at, expires_at, client_id, client_auth, 116 + device_id, parameters, details, code, current_refresh_token, scope, controller_did 117 + FROM oauth_token 118 + WHERE current_refresh_token = $1 119 + "#, 120 + refresh_token.as_str() 121 + ) 122 + .fetch_optional(&self.pool) 123 + .await 124 + .map_err(map_sqlx_error)?; 125 + match row { 126 + Some(r) => Ok(Some(( 127 + r.id, 128 + TokenData { 129 + did: r.did, 130 + token_id: r.token_id, 131 + created_at: r.created_at, 132 + updated_at: r.updated_at, 133 + expires_at: r.expires_at, 134 + client_id: r.client_id, 135 + client_auth: from_json(r.client_auth)?, 136 + device_id: r.device_id, 137 + parameters: from_json(r.parameters)?, 138 + details: r.details, 139 + code: r.code, 140 + current_refresh_token: r.current_refresh_token, 141 + scope: r.scope, 142 + controller_did: r.controller_did, 143 + }, 144 + ))), 145 + None => Ok(None), 146 + } 147 + } 148 + 149 + async fn get_token_by_previous_refresh_token( 150 + &self, 151 + refresh_token: &RefreshToken, 152 + ) -> Result<Option<(i32, TokenData)>, DbError> { 153 + let grace_cutoff = Utc::now() - Duration::seconds(REFRESH_GRACE_PERIOD_SECS); 154 + let row = sqlx::query!( 155 + r#" 156 + SELECT id, did, token_id, created_at, updated_at, expires_at, client_id, client_auth, 157 + device_id, parameters, details, code, current_refresh_token, scope, controller_did 158 + FROM oauth_token 159 + WHERE previous_refresh_token = $1 AND rotated_at > $2 160 + "#, 161 + refresh_token.as_str(), 162 + grace_cutoff 163 + ) 164 + .fetch_optional(&self.pool) 165 + .await 166 + .map_err(map_sqlx_error)?; 167 + match row { 168 + Some(r) => Ok(Some(( 169 + r.id, 170 + TokenData { 171 + did: r.did, 172 + token_id: r.token_id, 173 + created_at: r.created_at, 174 + updated_at: r.updated_at, 175 + expires_at: r.expires_at, 176 + client_id: r.client_id, 177 + client_auth: from_json(r.client_auth)?, 178 + device_id: r.device_id, 179 + parameters: from_json(r.parameters)?, 180 + details: r.details, 181 + code: r.code, 182 + current_refresh_token: r.current_refresh_token, 183 + scope: r.scope, 184 + controller_did: r.controller_did, 185 + }, 186 + ))), 187 + None => Ok(None), 188 + } 189 + } 190 + 191 + async fn rotate_token( 192 + &self, 193 + old_db_id: i32, 194 + new_refresh_token: &RefreshToken, 195 + new_expires_at: DateTime<Utc>, 196 + ) -> Result<(), DbError> { 197 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 198 + let old_refresh = sqlx::query_scalar!( 199 + r#" 200 + SELECT current_refresh_token FROM oauth_token WHERE id = $1 201 + "#, 202 + old_db_id 203 + ) 204 + .fetch_one(&mut *tx) 205 + .await 206 + .map_err(map_sqlx_error)?; 207 + if let Some(ref old_rt) = old_refresh { 208 + sqlx::query!( 209 + r#" 210 + INSERT INTO oauth_used_refresh_token (refresh_token, token_id) 211 + VALUES ($1, $2) 212 + "#, 213 + old_rt, 214 + old_db_id 215 + ) 216 + .execute(&mut *tx) 217 + .await 218 + .map_err(map_sqlx_error)?; 219 + } 220 + sqlx::query!( 221 + r#" 222 + UPDATE oauth_token 223 + SET current_refresh_token = $2, expires_at = $3, updated_at = NOW(), 224 + previous_refresh_token = $4, rotated_at = NOW() 225 + WHERE id = $1 226 + "#, 227 + old_db_id, 228 + new_refresh_token.as_str(), 229 + new_expires_at, 230 + old_refresh 231 + ) 232 + .execute(&mut *tx) 233 + .await 234 + .map_err(map_sqlx_error)?; 235 + tx.commit().await.map_err(map_sqlx_error)?; 236 + Ok(()) 237 + } 238 + 239 + async fn check_refresh_token_used(&self, refresh_token: &RefreshToken) -> Result<Option<i32>, DbError> { 240 + let row = sqlx::query_scalar!( 241 + r#" 242 + SELECT token_id FROM oauth_used_refresh_token WHERE refresh_token = $1 243 + "#, 244 + refresh_token.as_str() 245 + ) 246 + .fetch_optional(&self.pool) 247 + .await 248 + .map_err(map_sqlx_error)?; 249 + Ok(row) 250 + } 251 + 252 + async fn delete_token(&self, token_id: &TokenId) -> Result<(), DbError> { 253 + sqlx::query!( 254 + r#" 255 + DELETE FROM oauth_token WHERE token_id = $1 256 + "#, 257 + token_id.as_str() 258 + ) 259 + .execute(&self.pool) 260 + .await 261 + .map_err(map_sqlx_error)?; 262 + Ok(()) 263 + } 264 + 265 + async fn delete_token_family(&self, db_id: i32) -> Result<(), DbError> { 266 + sqlx::query!( 267 + r#" 268 + DELETE FROM oauth_token WHERE id = $1 269 + "#, 270 + db_id 271 + ) 272 + .execute(&self.pool) 273 + .await 274 + .map_err(map_sqlx_error)?; 275 + Ok(()) 276 + } 277 + 278 + async fn list_tokens_for_user(&self, did: &Did) -> Result<Vec<TokenData>, DbError> { 279 + let rows = sqlx::query!( 280 + r#" 281 + SELECT did, token_id, created_at, updated_at, expires_at, client_id, client_auth, 282 + device_id, parameters, details, code, current_refresh_token, scope, controller_did 283 + FROM oauth_token 284 + WHERE did = $1 285 + "#, 286 + did.as_str() 287 + ) 288 + .fetch_all(&self.pool) 289 + .await 290 + .map_err(map_sqlx_error)?; 291 + rows.into_iter() 292 + .map(|r| { 293 + Ok(TokenData { 294 + did: r.did, 295 + token_id: r.token_id, 296 + created_at: r.created_at, 297 + updated_at: r.updated_at, 298 + expires_at: r.expires_at, 299 + client_id: r.client_id, 300 + client_auth: from_json(r.client_auth)?, 301 + device_id: r.device_id, 302 + parameters: from_json(r.parameters)?, 303 + details: r.details, 304 + code: r.code, 305 + current_refresh_token: r.current_refresh_token, 306 + scope: r.scope, 307 + controller_did: r.controller_did, 308 + }) 309 + }) 310 + .collect() 311 + } 312 + 313 + async fn count_tokens_for_user(&self, did: &Did) -> Result<i64, DbError> { 314 + let count = sqlx::query_scalar!( 315 + r#" 316 + SELECT COUNT(*) as "count!" FROM oauth_token WHERE did = $1 317 + "#, 318 + did.as_str() 319 + ) 320 + .fetch_one(&self.pool) 321 + .await 322 + .map_err(map_sqlx_error)?; 323 + Ok(count) 324 + } 325 + 326 + async fn delete_oldest_tokens_for_user( 327 + &self, 328 + did: &Did, 329 + keep_count: i64, 330 + ) -> Result<u64, DbError> { 331 + let result = sqlx::query!( 332 + r#" 333 + DELETE FROM oauth_token 334 + WHERE id IN ( 335 + SELECT id FROM oauth_token 336 + WHERE did = $1 337 + ORDER BY updated_at ASC 338 + OFFSET $2 339 + ) 340 + "#, 341 + did.as_str(), 342 + keep_count 343 + ) 344 + .execute(&self.pool) 345 + .await 346 + .map_err(map_sqlx_error)?; 347 + Ok(result.rows_affected()) 348 + } 349 + 350 + async fn revoke_tokens_for_client(&self, did: &Did, client_id: &ClientId) -> Result<u64, DbError> { 351 + let result = sqlx::query!( 352 + "DELETE FROM oauth_token WHERE did = $1 AND client_id = $2", 353 + did.as_str(), 354 + client_id.as_str() 355 + ) 356 + .execute(&self.pool) 357 + .await 358 + .map_err(map_sqlx_error)?; 359 + Ok(result.rows_affected()) 360 + } 361 + 362 + async fn revoke_tokens_for_controller( 363 + &self, 364 + delegated_did: &Did, 365 + controller_did: &Did, 366 + ) -> Result<u64, DbError> { 367 + let result = sqlx::query!( 368 + "DELETE FROM oauth_token WHERE did = $1 AND controller_did = $2", 369 + delegated_did.as_str(), 370 + controller_did.as_str() 371 + ) 372 + .execute(&self.pool) 373 + .await 374 + .map_err(map_sqlx_error)?; 375 + Ok(result.rows_affected()) 376 + } 377 + 378 + async fn create_authorization_request( 379 + &self, 380 + request_id: &RequestId, 381 + data: &RequestData, 382 + ) -> Result<(), DbError> { 383 + let client_auth_json = match &data.client_auth { 384 + Some(ca) => Some(to_json(ca)?), 385 + None => None, 386 + }; 387 + let parameters_json = to_json(&data.parameters)?; 388 + sqlx::query!( 389 + r#" 390 + INSERT INTO oauth_authorization_request 391 + (id, did, device_id, client_id, client_auth, parameters, expires_at, code) 392 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 393 + "#, 394 + request_id.as_str(), 395 + data.did, 396 + data.device_id, 397 + data.client_id, 398 + client_auth_json, 399 + parameters_json, 400 + data.expires_at, 401 + data.code, 402 + ) 403 + .execute(&self.pool) 404 + .await 405 + .map_err(map_sqlx_error)?; 406 + Ok(()) 407 + } 408 + 409 + async fn get_authorization_request( 410 + &self, 411 + request_id: &RequestId, 412 + ) -> Result<Option<RequestData>, DbError> { 413 + let row = sqlx::query!( 414 + r#" 415 + SELECT did, device_id, client_id, client_auth, parameters, expires_at, code, controller_did 416 + FROM oauth_authorization_request 417 + WHERE id = $1 418 + "#, 419 + request_id.as_str() 420 + ) 421 + .fetch_optional(&self.pool) 422 + .await 423 + .map_err(map_sqlx_error)?; 424 + match row { 425 + Some(r) => { 426 + let client_auth: Option<ClientAuth> = match r.client_auth { 427 + Some(v) => Some(from_json(v)?), 428 + None => None, 429 + }; 430 + let parameters: AuthorizationRequestParameters = from_json(r.parameters)?; 431 + Ok(Some(RequestData { 432 + client_id: r.client_id, 433 + client_auth, 434 + parameters, 435 + expires_at: r.expires_at, 436 + did: r.did, 437 + device_id: r.device_id, 438 + code: r.code, 439 + controller_did: r.controller_did, 440 + })) 441 + } 442 + None => Ok(None), 443 + } 444 + } 445 + 446 + async fn set_authorization_did( 447 + &self, 448 + request_id: &RequestId, 449 + did: &Did, 450 + device_id: Option<&DeviceId>, 451 + ) -> Result<(), DbError> { 452 + sqlx::query!( 453 + r#" 454 + UPDATE oauth_authorization_request 455 + SET did = $2, device_id = $3 456 + WHERE id = $1 457 + "#, 458 + request_id.as_str(), 459 + did.as_str(), 460 + device_id.map(|d| d.as_str()) 461 + ) 462 + .execute(&self.pool) 463 + .await 464 + .map_err(map_sqlx_error)?; 465 + Ok(()) 466 + } 467 + 468 + async fn update_authorization_request( 469 + &self, 470 + request_id: &RequestId, 471 + did: &Did, 472 + device_id: Option<&DeviceId>, 473 + code: &AuthorizationCode, 474 + ) -> Result<(), DbError> { 475 + sqlx::query!( 476 + r#" 477 + UPDATE oauth_authorization_request 478 + SET did = $2, device_id = $3, code = $4 479 + WHERE id = $1 480 + "#, 481 + request_id.as_str(), 482 + did.as_str(), 483 + device_id.map(|d| d.as_str()), 484 + code.as_str() 485 + ) 486 + .execute(&self.pool) 487 + .await 488 + .map_err(map_sqlx_error)?; 489 + Ok(()) 490 + } 491 + 492 + async fn consume_authorization_request_by_code( 493 + &self, 494 + code: &AuthorizationCode, 495 + ) -> Result<Option<RequestData>, DbError> { 496 + let row = sqlx::query!( 497 + r#" 498 + DELETE FROM oauth_authorization_request 499 + WHERE code = $1 500 + RETURNING did, device_id, client_id, client_auth, parameters, expires_at, code, controller_did 501 + "#, 502 + code.as_str() 503 + ) 504 + .fetch_optional(&self.pool) 505 + .await 506 + .map_err(map_sqlx_error)?; 507 + match row { 508 + Some(r) => { 509 + let client_auth: Option<ClientAuth> = match r.client_auth { 510 + Some(v) => Some(from_json(v)?), 511 + None => None, 512 + }; 513 + let parameters: AuthorizationRequestParameters = from_json(r.parameters)?; 514 + Ok(Some(RequestData { 515 + client_id: r.client_id, 516 + client_auth, 517 + parameters, 518 + expires_at: r.expires_at, 519 + did: r.did, 520 + device_id: r.device_id, 521 + code: r.code, 522 + controller_did: r.controller_did, 523 + })) 524 + } 525 + None => Ok(None), 526 + } 527 + } 528 + 529 + async fn delete_authorization_request(&self, request_id: &RequestId) -> Result<(), DbError> { 530 + sqlx::query!( 531 + r#" 532 + DELETE FROM oauth_authorization_request WHERE id = $1 533 + "#, 534 + request_id.as_str() 535 + ) 536 + .execute(&self.pool) 537 + .await 538 + .map_err(map_sqlx_error)?; 539 + Ok(()) 540 + } 541 + 542 + async fn delete_expired_authorization_requests(&self) -> Result<u64, DbError> { 543 + let result = sqlx::query!( 544 + r#" 545 + DELETE FROM oauth_authorization_request 546 + WHERE expires_at < NOW() 547 + "# 548 + ) 549 + .execute(&self.pool) 550 + .await 551 + .map_err(map_sqlx_error)?; 552 + Ok(result.rows_affected()) 553 + } 554 + 555 + async fn mark_request_authenticated( 556 + &self, 557 + request_id: &RequestId, 558 + did: &Did, 559 + device_id: Option<&DeviceId>, 560 + ) -> Result<(), DbError> { 561 + sqlx::query!( 562 + r#" 563 + UPDATE oauth_authorization_request 564 + SET did = $2, device_id = $3 565 + WHERE id = $1 566 + "#, 567 + request_id.as_str(), 568 + did.as_str(), 569 + device_id.map(|d| d.as_str()) 570 + ) 571 + .execute(&self.pool) 572 + .await 573 + .map_err(map_sqlx_error)?; 574 + Ok(()) 575 + } 576 + 577 + async fn update_request_scope(&self, request_id: &RequestId, scope: &str) -> Result<(), DbError> { 578 + sqlx::query!( 579 + r#" 580 + UPDATE oauth_authorization_request 581 + SET parameters = jsonb_set(parameters, '{scope}', to_jsonb($2::text)) 582 + WHERE id = $1 583 + "#, 584 + request_id.as_str(), 585 + scope 586 + ) 587 + .execute(&self.pool) 588 + .await 589 + .map_err(map_sqlx_error)?; 590 + Ok(()) 591 + } 592 + 593 + async fn set_controller_did( 594 + &self, 595 + request_id: &RequestId, 596 + controller_did: &Did, 597 + ) -> Result<(), DbError> { 598 + sqlx::query!( 599 + r#" 600 + UPDATE oauth_authorization_request 601 + SET controller_did = $2 602 + WHERE id = $1 603 + "#, 604 + request_id.as_str(), 605 + controller_did.as_str() 606 + ) 607 + .execute(&self.pool) 608 + .await 609 + .map_err(map_sqlx_error)?; 610 + Ok(()) 611 + } 612 + 613 + async fn set_request_did(&self, request_id: &RequestId, did: &Did) -> Result<(), DbError> { 614 + sqlx::query!( 615 + r#" 616 + UPDATE oauth_authorization_request 617 + SET did = $2 618 + WHERE id = $1 619 + "#, 620 + request_id.as_str(), 621 + did.as_str() 622 + ) 623 + .execute(&self.pool) 624 + .await 625 + .map_err(map_sqlx_error)?; 626 + Ok(()) 627 + } 628 + 629 + async fn create_device(&self, device_id: &DeviceId, data: &DeviceData) -> Result<(), DbError> { 630 + sqlx::query!( 631 + r#" 632 + INSERT INTO oauth_device (id, session_id, user_agent, ip_address, last_seen_at) 633 + VALUES ($1, $2, $3, $4, $5) 634 + "#, 635 + device_id.as_str(), 636 + data.session_id, 637 + data.user_agent, 638 + data.ip_address, 639 + data.last_seen_at, 640 + ) 641 + .execute(&self.pool) 642 + .await 643 + .map_err(map_sqlx_error)?; 644 + Ok(()) 645 + } 646 + 647 + async fn get_device(&self, device_id: &DeviceId) -> Result<Option<DeviceData>, DbError> { 648 + let row = sqlx::query!( 649 + r#" 650 + SELECT session_id, user_agent, ip_address, last_seen_at 651 + FROM oauth_device 652 + WHERE id = $1 653 + "#, 654 + device_id.as_str() 655 + ) 656 + .fetch_optional(&self.pool) 657 + .await 658 + .map_err(map_sqlx_error)?; 659 + Ok(row.map(|r| DeviceData { 660 + session_id: r.session_id, 661 + user_agent: r.user_agent, 662 + ip_address: r.ip_address, 663 + last_seen_at: r.last_seen_at, 664 + })) 665 + } 666 + 667 + async fn update_device_last_seen(&self, device_id: &DeviceId) -> Result<(), DbError> { 668 + sqlx::query!( 669 + r#" 670 + UPDATE oauth_device 671 + SET last_seen_at = NOW() 672 + WHERE id = $1 673 + "#, 674 + device_id.as_str() 675 + ) 676 + .execute(&self.pool) 677 + .await 678 + .map_err(map_sqlx_error)?; 679 + Ok(()) 680 + } 681 + 682 + async fn delete_device(&self, device_id: &DeviceId) -> Result<(), DbError> { 683 + sqlx::query!( 684 + r#" 685 + DELETE FROM oauth_device WHERE id = $1 686 + "#, 687 + device_id.as_str() 688 + ) 689 + .execute(&self.pool) 690 + .await 691 + .map_err(map_sqlx_error)?; 692 + Ok(()) 693 + } 694 + 695 + async fn upsert_account_device(&self, did: &Did, device_id: &DeviceId) -> Result<(), DbError> { 696 + sqlx::query!( 697 + r#" 698 + INSERT INTO oauth_account_device (did, device_id, created_at, updated_at) 699 + VALUES ($1, $2, NOW(), NOW()) 700 + ON CONFLICT (did, device_id) DO UPDATE SET updated_at = NOW() 701 + "#, 702 + did.as_str(), 703 + device_id.as_str() 704 + ) 705 + .execute(&self.pool) 706 + .await 707 + .map_err(map_sqlx_error)?; 708 + Ok(()) 709 + } 710 + 711 + async fn get_device_accounts(&self, device_id: &DeviceId) -> Result<Vec<DeviceAccountRow>, DbError> { 712 + let rows = sqlx::query!( 713 + r#" 714 + SELECT u.did, u.handle, u.email, ad.updated_at as last_used_at 715 + FROM oauth_account_device ad 716 + JOIN users u ON u.did = ad.did 717 + WHERE ad.device_id = $1 718 + AND u.deactivated_at IS NULL 719 + AND u.takedown_ref IS NULL 720 + ORDER BY ad.updated_at DESC 721 + "#, 722 + device_id.as_str() 723 + ) 724 + .fetch_all(&self.pool) 725 + .await 726 + .map_err(map_sqlx_error)?; 727 + Ok(rows 728 + .into_iter() 729 + .map(|r| DeviceAccountRow { 730 + did: Did::from(r.did), 731 + handle: Handle::from(r.handle), 732 + email: r.email, 733 + last_used_at: r.last_used_at, 734 + }) 735 + .collect()) 736 + } 737 + 738 + async fn verify_account_on_device(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError> { 739 + let row = sqlx::query!( 740 + r#" 741 + SELECT 1 as "exists!" 742 + FROM oauth_account_device ad 743 + JOIN users u ON u.did = ad.did 744 + WHERE ad.device_id = $1 745 + AND ad.did = $2 746 + AND u.deactivated_at IS NULL 747 + AND u.takedown_ref IS NULL 748 + "#, 749 + device_id.as_str(), 750 + did.as_str() 751 + ) 752 + .fetch_optional(&self.pool) 753 + .await 754 + .map_err(map_sqlx_error)?; 755 + Ok(row.is_some()) 756 + } 757 + 758 + async fn check_and_record_dpop_jti(&self, jti: &DPoPProofId) -> Result<bool, DbError> { 759 + let result = sqlx::query!( 760 + r#" 761 + INSERT INTO oauth_dpop_jti (jti) 762 + VALUES ($1) 763 + ON CONFLICT (jti) DO NOTHING 764 + "#, 765 + jti.as_str() 766 + ) 767 + .execute(&self.pool) 768 + .await 769 + .map_err(map_sqlx_error)?; 770 + Ok(result.rows_affected() > 0) 771 + } 772 + 773 + async fn cleanup_expired_dpop_jtis(&self, max_age_secs: i64) -> Result<u64, DbError> { 774 + let result = sqlx::query!( 775 + r#" 776 + DELETE FROM oauth_dpop_jti 777 + WHERE created_at < NOW() - INTERVAL '1 second' * $1 778 + "#, 779 + max_age_secs as f64 780 + ) 781 + .execute(&self.pool) 782 + .await 783 + .map_err(map_sqlx_error)?; 784 + Ok(result.rows_affected()) 785 + } 786 + 787 + async fn create_2fa_challenge( 788 + &self, 789 + did: &Did, 790 + request_uri: &RequestId, 791 + ) -> Result<TwoFactorChallenge, DbError> { 792 + let code = { 793 + let mut rng = rand::thread_rng(); 794 + let code_num: u32 = rng.gen_range(0..1_000_000); 795 + format!("{:06}", code_num) 796 + }; 797 + let expires_at = Utc::now() + Duration::minutes(10); 798 + let row = sqlx::query!( 799 + r#" 800 + INSERT INTO oauth_2fa_challenge (did, request_uri, code, expires_at) 801 + VALUES ($1, $2, $3, $4) 802 + RETURNING id, did, request_uri, code, attempts, created_at, expires_at 803 + "#, 804 + did.as_str(), 805 + request_uri.as_str(), 806 + code, 807 + expires_at, 808 + ) 809 + .fetch_one(&self.pool) 810 + .await 811 + .map_err(map_sqlx_error)?; 812 + Ok(TwoFactorChallenge { 813 + id: row.id, 814 + did: Did::from(row.did), 815 + request_uri: row.request_uri, 816 + code: row.code, 817 + attempts: row.attempts, 818 + created_at: row.created_at, 819 + expires_at: row.expires_at, 820 + }) 821 + } 822 + 823 + async fn get_2fa_challenge( 824 + &self, 825 + request_uri: &RequestId, 826 + ) -> Result<Option<TwoFactorChallenge>, DbError> { 827 + let row = sqlx::query!( 828 + r#" 829 + SELECT id, did, request_uri, code, attempts, created_at, expires_at 830 + FROM oauth_2fa_challenge 831 + WHERE request_uri = $1 832 + "#, 833 + request_uri.as_str() 834 + ) 835 + .fetch_optional(&self.pool) 836 + .await 837 + .map_err(map_sqlx_error)?; 838 + Ok(row.map(|r| TwoFactorChallenge { 839 + id: r.id, 840 + did: Did::from(r.did), 841 + request_uri: r.request_uri, 842 + code: r.code, 843 + attempts: r.attempts, 844 + created_at: r.created_at, 845 + expires_at: r.expires_at, 846 + })) 847 + } 848 + 849 + async fn increment_2fa_attempts(&self, id: Uuid) -> Result<i32, DbError> { 850 + let row = sqlx::query!( 851 + r#" 852 + UPDATE oauth_2fa_challenge 853 + SET attempts = attempts + 1 854 + WHERE id = $1 855 + RETURNING attempts 856 + "#, 857 + id 858 + ) 859 + .fetch_one(&self.pool) 860 + .await 861 + .map_err(map_sqlx_error)?; 862 + Ok(row.attempts) 863 + } 864 + 865 + async fn delete_2fa_challenge(&self, id: Uuid) -> Result<(), DbError> { 866 + sqlx::query!( 867 + r#" 868 + DELETE FROM oauth_2fa_challenge WHERE id = $1 869 + "#, 870 + id 871 + ) 872 + .execute(&self.pool) 873 + .await 874 + .map_err(map_sqlx_error)?; 875 + Ok(()) 876 + } 877 + 878 + async fn delete_2fa_challenge_by_request_uri(&self, request_uri: &RequestId) -> Result<(), DbError> { 879 + sqlx::query!( 880 + r#" 881 + DELETE FROM oauth_2fa_challenge WHERE request_uri = $1 882 + "#, 883 + request_uri.as_str() 884 + ) 885 + .execute(&self.pool) 886 + .await 887 + .map_err(map_sqlx_error)?; 888 + Ok(()) 889 + } 890 + 891 + async fn cleanup_expired_2fa_challenges(&self) -> Result<u64, DbError> { 892 + let result = sqlx::query!( 893 + r#" 894 + DELETE FROM oauth_2fa_challenge WHERE expires_at < NOW() 895 + "# 896 + ) 897 + .execute(&self.pool) 898 + .await 899 + .map_err(map_sqlx_error)?; 900 + Ok(result.rows_affected()) 901 + } 902 + 903 + async fn check_user_2fa_enabled(&self, did: &Did) -> Result<bool, DbError> { 904 + let row = sqlx::query!( 905 + r#" 906 + SELECT two_factor_enabled 907 + FROM users 908 + WHERE did = $1 909 + "#, 910 + did.as_str() 911 + ) 912 + .fetch_optional(&self.pool) 913 + .await 914 + .map_err(map_sqlx_error)?; 915 + Ok(row.map(|r| r.two_factor_enabled).unwrap_or(false)) 916 + } 917 + 918 + async fn get_scope_preferences( 919 + &self, 920 + did: &Did, 921 + client_id: &ClientId, 922 + ) -> Result<Vec<ScopePreference>, DbError> { 923 + let rows = sqlx::query!( 924 + r#" 925 + SELECT scope, granted FROM oauth_scope_preference 926 + WHERE did = $1 AND client_id = $2 927 + "#, 928 + did.as_str(), 929 + client_id.as_str() 930 + ) 931 + .fetch_all(&self.pool) 932 + .await 933 + .map_err(map_sqlx_error)?; 934 + 935 + Ok(rows 936 + .into_iter() 937 + .map(|r| ScopePreference { 938 + scope: r.scope, 939 + granted: r.granted, 940 + }) 941 + .collect()) 942 + } 943 + 944 + async fn upsert_scope_preferences( 945 + &self, 946 + did: &Did, 947 + client_id: &ClientId, 948 + prefs: &[ScopePreference], 949 + ) -> Result<(), DbError> { 950 + for pref in prefs { 951 + sqlx::query!( 952 + r#" 953 + INSERT INTO oauth_scope_preference (did, client_id, scope, granted, created_at, updated_at) 954 + VALUES ($1, $2, $3, $4, NOW(), NOW()) 955 + ON CONFLICT (did, client_id, scope) DO UPDATE SET granted = $4, updated_at = NOW() 956 + "#, 957 + did.as_str(), 958 + client_id.as_str(), 959 + pref.scope, 960 + pref.granted 961 + ) 962 + .execute(&self.pool) 963 + .await 964 + .map_err(map_sqlx_error)?; 965 + } 966 + Ok(()) 967 + } 968 + 969 + async fn delete_scope_preferences(&self, did: &Did, client_id: &ClientId) -> Result<(), DbError> { 970 + sqlx::query!( 971 + r#" 972 + DELETE FROM oauth_scope_preference 973 + WHERE did = $1 AND client_id = $2 974 + "#, 975 + did.as_str(), 976 + client_id.as_str() 977 + ) 978 + .execute(&self.pool) 979 + .await 980 + .map_err(map_sqlx_error)?; 981 + Ok(()) 982 + } 983 + 984 + async fn upsert_authorized_client( 985 + &self, 986 + did: &Did, 987 + client_id: &ClientId, 988 + data: &AuthorizedClientData, 989 + ) -> Result<(), DbError> { 990 + let data_json = to_json(data)?; 991 + sqlx::query!( 992 + r#" 993 + INSERT INTO oauth_authorized_client (did, client_id, created_at, updated_at, data) 994 + VALUES ($1, $2, NOW(), NOW(), $3) 995 + ON CONFLICT (did, client_id) DO UPDATE SET updated_at = NOW(), data = $3 996 + "#, 997 + did.as_str(), 998 + client_id.as_str(), 999 + data_json 1000 + ) 1001 + .execute(&self.pool) 1002 + .await 1003 + .map_err(map_sqlx_error)?; 1004 + Ok(()) 1005 + } 1006 + 1007 + async fn get_authorized_client( 1008 + &self, 1009 + did: &Did, 1010 + client_id: &ClientId, 1011 + ) -> Result<Option<AuthorizedClientData>, DbError> { 1012 + let row = sqlx::query_scalar!( 1013 + r#" 1014 + SELECT data FROM oauth_authorized_client 1015 + WHERE did = $1 AND client_id = $2 1016 + "#, 1017 + did.as_str(), 1018 + client_id.as_str() 1019 + ) 1020 + .fetch_optional(&self.pool) 1021 + .await 1022 + .map_err(map_sqlx_error)?; 1023 + match row { 1024 + Some(v) => Ok(Some(from_json(v)?)), 1025 + None => Ok(None), 1026 + } 1027 + } 1028 + 1029 + async fn list_trusted_devices(&self, did: &Did) -> Result<Vec<TrustedDeviceRow>, DbError> { 1030 + let rows = sqlx::query!( 1031 + r#"SELECT od.id, od.user_agent, od.friendly_name, od.trusted_at, od.trusted_until, od.last_seen_at 1032 + FROM oauth_device od 1033 + JOIN oauth_account_device oad ON od.id = oad.device_id 1034 + WHERE oad.did = $1 AND od.trusted_until IS NOT NULL AND od.trusted_until > NOW() 1035 + ORDER BY od.last_seen_at DESC"#, 1036 + did.as_str() 1037 + ) 1038 + .fetch_all(&self.pool) 1039 + .await 1040 + .map_err(map_sqlx_error)?; 1041 + 1042 + Ok(rows 1043 + .into_iter() 1044 + .map(|r| TrustedDeviceRow { 1045 + id: r.id, 1046 + user_agent: r.user_agent, 1047 + friendly_name: r.friendly_name, 1048 + trusted_at: r.trusted_at, 1049 + trusted_until: r.trusted_until, 1050 + last_seen_at: r.last_seen_at, 1051 + }) 1052 + .collect()) 1053 + } 1054 + 1055 + async fn get_device_trust_info( 1056 + &self, 1057 + device_id: &DeviceId, 1058 + did: &Did, 1059 + ) -> Result<Option<DeviceTrustInfo>, DbError> { 1060 + let row = sqlx::query!( 1061 + r#"SELECT trusted_at, trusted_until FROM oauth_device od 1062 + JOIN oauth_account_device oad ON od.id = oad.device_id 1063 + WHERE od.id = $1 AND oad.did = $2"#, 1064 + device_id.as_str(), 1065 + did.as_str() 1066 + ) 1067 + .fetch_optional(&self.pool) 1068 + .await 1069 + .map_err(map_sqlx_error)?; 1070 + 1071 + Ok(row.map(|r| DeviceTrustInfo { 1072 + trusted_at: r.trusted_at, 1073 + trusted_until: r.trusted_until, 1074 + })) 1075 + } 1076 + 1077 + async fn device_belongs_to_user(&self, device_id: &DeviceId, did: &Did) -> Result<bool, DbError> { 1078 + let exists = sqlx::query_scalar!( 1079 + r#"SELECT 1 as "one!" FROM oauth_device od 1080 + JOIN oauth_account_device oad ON od.id = oad.device_id 1081 + WHERE oad.did = $1 AND od.id = $2"#, 1082 + did.as_str(), 1083 + device_id.as_str() 1084 + ) 1085 + .fetch_optional(&self.pool) 1086 + .await 1087 + .map_err(map_sqlx_error)?; 1088 + 1089 + Ok(exists.is_some()) 1090 + } 1091 + 1092 + async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError> { 1093 + sqlx::query!( 1094 + "UPDATE oauth_device SET trusted_at = NULL, trusted_until = NULL WHERE id = $1", 1095 + device_id.as_str() 1096 + ) 1097 + .execute(&self.pool) 1098 + .await 1099 + .map_err(map_sqlx_error)?; 1100 + Ok(()) 1101 + } 1102 + 1103 + async fn update_device_friendly_name( 1104 + &self, 1105 + device_id: &DeviceId, 1106 + friendly_name: Option<&str>, 1107 + ) -> Result<(), DbError> { 1108 + sqlx::query!( 1109 + "UPDATE oauth_device SET friendly_name = $1 WHERE id = $2", 1110 + friendly_name, 1111 + device_id.as_str() 1112 + ) 1113 + .execute(&self.pool) 1114 + .await 1115 + .map_err(map_sqlx_error)?; 1116 + Ok(()) 1117 + } 1118 + 1119 + async fn trust_device( 1120 + &self, 1121 + device_id: &DeviceId, 1122 + trusted_at: DateTime<Utc>, 1123 + trusted_until: DateTime<Utc>, 1124 + ) -> Result<(), DbError> { 1125 + sqlx::query!( 1126 + "UPDATE oauth_device SET trusted_at = $1, trusted_until = $2 WHERE id = $3", 1127 + trusted_at, 1128 + trusted_until, 1129 + device_id.as_str() 1130 + ) 1131 + .execute(&self.pool) 1132 + .await 1133 + .map_err(map_sqlx_error)?; 1134 + Ok(()) 1135 + } 1136 + 1137 + async fn extend_device_trust( 1138 + &self, 1139 + device_id: &DeviceId, 1140 + trusted_until: DateTime<Utc>, 1141 + ) -> Result<(), DbError> { 1142 + sqlx::query!( 1143 + "UPDATE oauth_device SET trusted_until = $1 WHERE id = $2 AND trusted_until IS NOT NULL", 1144 + trusted_until, 1145 + device_id.as_str() 1146 + ) 1147 + .execute(&self.pool) 1148 + .await 1149 + .map_err(map_sqlx_error)?; 1150 + Ok(()) 1151 + } 1152 + 1153 + async fn list_sessions_by_did(&self, did: &Did) -> Result<Vec<OAuthSessionListItem>, DbError> { 1154 + let rows = sqlx::query!( 1155 + r#" 1156 + SELECT id, token_id, created_at, expires_at, client_id 1157 + FROM oauth_token 1158 + WHERE did = $1 AND expires_at > NOW() 1159 + ORDER BY created_at DESC 1160 + "#, 1161 + did.as_str() 1162 + ) 1163 + .fetch_all(&self.pool) 1164 + .await 1165 + .map_err(map_sqlx_error)?; 1166 + 1167 + Ok(rows 1168 + .into_iter() 1169 + .map(|r| OAuthSessionListItem { 1170 + id: r.id, 1171 + token_id: TokenId::from(r.token_id), 1172 + created_at: r.created_at, 1173 + expires_at: r.expires_at, 1174 + client_id: ClientId::from(r.client_id), 1175 + }) 1176 + .collect()) 1177 + } 1178 + 1179 + async fn delete_session_by_id(&self, session_id: i32, did: &Did) -> Result<u64, DbError> { 1180 + let result = sqlx::query!( 1181 + "DELETE FROM oauth_token WHERE id = $1 AND did = $2", 1182 + session_id, 1183 + did.as_str() 1184 + ) 1185 + .execute(&self.pool) 1186 + .await 1187 + .map_err(map_sqlx_error)?; 1188 + Ok(result.rows_affected()) 1189 + } 1190 + 1191 + async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError> { 1192 + let result = sqlx::query!("DELETE FROM oauth_token WHERE did = $1", did.as_str()) 1193 + .execute(&self.pool) 1194 + .await 1195 + .map_err(map_sqlx_error)?; 1196 + Ok(result.rows_affected()) 1197 + } 1198 + 1199 + async fn delete_sessions_by_did_except( 1200 + &self, 1201 + did: &Did, 1202 + except_token_id: &TokenId, 1203 + ) -> Result<u64, DbError> { 1204 + let result = sqlx::query!( 1205 + "DELETE FROM oauth_token WHERE did = $1 AND token_id != $2", 1206 + did.as_str(), 1207 + except_token_id.as_str() 1208 + ) 1209 + .execute(&self.pool) 1210 + .await 1211 + .map_err(map_sqlx_error)?; 1212 + Ok(result.rows_affected()) 1213 + } 1214 + }
+1447
crates/tranquil-db/src/postgres/repo.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use sqlx::PgPool; 4 + use tranquil_db_traits::{ 5 + BrokenGenesisCommit, CommitEventData, DbError, EventBlocksCids, FullRecordInfo, ImportBlock, 6 + ImportRecord, ImportRepoError, RecordInfo, RecordWithTakedown, RepoAccountInfo, RepoInfo, 7 + RepoListItem, RepoRepository, RepoWithoutRev, SequencedEvent, UserNeedingRecordBlobsBackfill, 8 + UserWithoutBlocks, 9 + }; 10 + use tranquil_types::{AtUri, CidLink, Did, Handle, Nsid, Rkey}; 11 + use uuid::Uuid; 12 + 13 + use super::user::map_sqlx_error; 14 + 15 + struct RecordRow { 16 + rkey: String, 17 + record_cid: String, 18 + } 19 + 20 + struct SequencedEventRow { 21 + seq: i64, 22 + did: String, 23 + created_at: DateTime<Utc>, 24 + event_type: String, 25 + commit_cid: Option<String>, 26 + prev_cid: Option<String>, 27 + prev_data_cid: Option<String>, 28 + ops: Option<serde_json::Value>, 29 + blobs: Option<Vec<String>>, 30 + blocks_cids: Option<Vec<String>>, 31 + handle: Option<String>, 32 + active: Option<bool>, 33 + status: Option<String>, 34 + rev: Option<String>, 35 + } 36 + 37 + pub struct PostgresRepoRepository { 38 + pool: PgPool, 39 + } 40 + 41 + impl PostgresRepoRepository { 42 + pub fn new(pool: PgPool) -> Self { 43 + Self { pool } 44 + } 45 + } 46 + 47 + #[async_trait] 48 + impl RepoRepository for PostgresRepoRepository { 49 + async fn create_repo( 50 + &self, 51 + user_id: Uuid, 52 + repo_root_cid: &CidLink, 53 + repo_rev: &str, 54 + ) -> Result<(), DbError> { 55 + sqlx::query!( 56 + "INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)", 57 + user_id, 58 + repo_root_cid.as_str(), 59 + repo_rev 60 + ) 61 + .execute(&self.pool) 62 + .await 63 + .map_err(map_sqlx_error)?; 64 + 65 + Ok(()) 66 + } 67 + 68 + async fn update_repo_root( 69 + &self, 70 + user_id: Uuid, 71 + repo_root_cid: &CidLink, 72 + repo_rev: &str, 73 + ) -> Result<(), DbError> { 74 + sqlx::query!( 75 + "UPDATE repos SET repo_root_cid = $1, repo_rev = $2, updated_at = NOW() WHERE user_id = $3", 76 + repo_root_cid.as_str(), 77 + repo_rev, 78 + user_id 79 + ) 80 + .execute(&self.pool) 81 + .await 82 + .map_err(map_sqlx_error)?; 83 + 84 + Ok(()) 85 + } 86 + 87 + async fn update_repo_rev(&self, user_id: Uuid, repo_rev: &str) -> Result<(), DbError> { 88 + sqlx::query!( 89 + "UPDATE repos SET repo_rev = $1 WHERE user_id = $2", 90 + repo_rev, 91 + user_id 92 + ) 93 + .execute(&self.pool) 94 + .await 95 + .map_err(map_sqlx_error)?; 96 + 97 + Ok(()) 98 + } 99 + 100 + async fn delete_repo(&self, user_id: Uuid) -> Result<(), DbError> { 101 + sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id) 102 + .execute(&self.pool) 103 + .await 104 + .map_err(map_sqlx_error)?; 105 + 106 + Ok(()) 107 + } 108 + 109 + async fn get_repo_root_for_update(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError> { 110 + let result = sqlx::query_scalar!( 111 + "SELECT repo_root_cid FROM repos WHERE user_id = $1 FOR UPDATE NOWAIT", 112 + user_id 113 + ) 114 + .fetch_optional(&self.pool) 115 + .await 116 + .map_err(map_sqlx_error)?; 117 + 118 + Ok(result.map(CidLink::from)) 119 + } 120 + 121 + async fn get_repo(&self, user_id: Uuid) -> Result<Option<RepoInfo>, DbError> { 122 + let row = sqlx::query!( 123 + "SELECT user_id, repo_root_cid, repo_rev FROM repos WHERE user_id = $1", 124 + user_id 125 + ) 126 + .fetch_optional(&self.pool) 127 + .await 128 + .map_err(map_sqlx_error)?; 129 + 130 + Ok(row.map(|r| RepoInfo { 131 + user_id: r.user_id, 132 + repo_root_cid: CidLink::from(r.repo_root_cid), 133 + repo_rev: r.repo_rev, 134 + })) 135 + } 136 + 137 + async fn get_repo_root_by_did(&self, did: &Did) -> Result<Option<CidLink>, DbError> { 138 + let result = sqlx::query_scalar!( 139 + "SELECT r.repo_root_cid FROM repos r JOIN users u ON r.user_id = u.id WHERE u.did = $1", 140 + did.as_str() 141 + ) 142 + .fetch_optional(&self.pool) 143 + .await 144 + .map_err(map_sqlx_error)?; 145 + 146 + Ok(result.map(CidLink::from)) 147 + } 148 + 149 + async fn count_repos(&self) -> Result<i64, DbError> { 150 + let count = 151 + sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!" FROM repos"#) 152 + .fetch_one(&self.pool) 153 + .await 154 + .map_err(map_sqlx_error)?; 155 + 156 + Ok(count) 157 + } 158 + 159 + async fn get_repos_without_rev(&self) -> Result<Vec<RepoWithoutRev>, DbError> { 160 + let rows = sqlx::query!( 161 + "SELECT user_id, repo_root_cid FROM repos WHERE repo_rev IS NULL" 162 + ) 163 + .fetch_all(&self.pool) 164 + .await 165 + .map_err(map_sqlx_error)?; 166 + 167 + Ok(rows 168 + .into_iter() 169 + .map(|r| RepoWithoutRev { 170 + user_id: r.user_id, 171 + repo_root_cid: CidLink::from(r.repo_root_cid), 172 + }) 173 + .collect()) 174 + } 175 + 176 + async fn upsert_records( 177 + &self, 178 + repo_id: Uuid, 179 + collections: &[Nsid], 180 + rkeys: &[Rkey], 181 + record_cids: &[CidLink], 182 + repo_rev: &str, 183 + ) -> Result<(), DbError> { 184 + let collections_str: Vec<&str> = collections.iter().map(|c| c.as_str()).collect(); 185 + let rkeys_str: Vec<&str> = rkeys.iter().map(|r| r.as_str()).collect(); 186 + let cids_str: Vec<&str> = record_cids.iter().map(|c| c.as_str()).collect(); 187 + 188 + sqlx::query!( 189 + r#" 190 + INSERT INTO records (repo_id, collection, rkey, record_cid, repo_rev) 191 + SELECT $1, collection, rkey, record_cid, $5 192 + FROM UNNEST($2::text[], $3::text[], $4::text[]) AS t(collection, rkey, record_cid) 193 + ON CONFLICT (repo_id, collection, rkey) DO UPDATE 194 + SET record_cid = EXCLUDED.record_cid, repo_rev = EXCLUDED.repo_rev, created_at = NOW() 195 + "#, 196 + repo_id, 197 + &collections_str as &[&str], 198 + &rkeys_str as &[&str], 199 + &cids_str as &[&str], 200 + repo_rev 201 + ) 202 + .execute(&self.pool) 203 + .await 204 + .map_err(map_sqlx_error)?; 205 + 206 + Ok(()) 207 + } 208 + 209 + async fn delete_records( 210 + &self, 211 + repo_id: Uuid, 212 + collections: &[Nsid], 213 + rkeys: &[Rkey], 214 + ) -> Result<(), DbError> { 215 + let collections_str: Vec<&str> = collections.iter().map(|c| c.as_str()).collect(); 216 + let rkeys_str: Vec<&str> = rkeys.iter().map(|r| r.as_str()).collect(); 217 + 218 + sqlx::query!( 219 + r#" 220 + DELETE FROM records 221 + WHERE repo_id = $1 222 + AND (collection, rkey) IN (SELECT * FROM UNNEST($2::text[], $3::text[])) 223 + "#, 224 + repo_id, 225 + &collections_str as &[&str], 226 + &rkeys_str as &[&str] 227 + ) 228 + .execute(&self.pool) 229 + .await 230 + .map_err(map_sqlx_error)?; 231 + 232 + Ok(()) 233 + } 234 + 235 + async fn delete_all_records(&self, repo_id: Uuid) -> Result<(), DbError> { 236 + sqlx::query!("DELETE FROM records WHERE repo_id = $1", repo_id) 237 + .execute(&self.pool) 238 + .await 239 + .map_err(map_sqlx_error)?; 240 + 241 + Ok(()) 242 + } 243 + 244 + async fn get_record_cid( 245 + &self, 246 + repo_id: Uuid, 247 + collection: &Nsid, 248 + rkey: &Rkey, 249 + ) -> Result<Option<CidLink>, DbError> { 250 + let result = sqlx::query_scalar!( 251 + "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3", 252 + repo_id, 253 + collection.as_str(), 254 + rkey.as_str() 255 + ) 256 + .fetch_optional(&self.pool) 257 + .await 258 + .map_err(map_sqlx_error)?; 259 + 260 + Ok(result.map(CidLink::from)) 261 + } 262 + 263 + async fn list_records( 264 + &self, 265 + repo_id: Uuid, 266 + collection: &Nsid, 267 + cursor: Option<&Rkey>, 268 + limit: i64, 269 + reverse: bool, 270 + rkey_start: Option<&Rkey>, 271 + rkey_end: Option<&Rkey>, 272 + ) -> Result<Vec<RecordInfo>, DbError> { 273 + let to_record_info = |rows: Vec<RecordRow>| { 274 + rows.into_iter() 275 + .map(|r| RecordInfo { 276 + rkey: Rkey::from(r.rkey), 277 + record_cid: CidLink::from(r.record_cid), 278 + }) 279 + .collect() 280 + }; 281 + 282 + let collection_str = collection.as_str(); 283 + 284 + if let Some(cursor_val) = cursor { 285 + let cursor_str = cursor_val.as_str(); 286 + return match reverse { 287 + false => { 288 + let rows = sqlx::query_as!( 289 + RecordRow, 290 + r#"SELECT rkey, record_cid FROM records 291 + WHERE repo_id = $1 AND collection = $2 AND rkey < $3 292 + ORDER BY rkey DESC LIMIT $4"#, 293 + repo_id, 294 + collection_str, 295 + cursor_str, 296 + limit 297 + ) 298 + .fetch_all(&self.pool) 299 + .await 300 + .map_err(map_sqlx_error)?; 301 + Ok(to_record_info(rows)) 302 + } 303 + true => { 304 + let rows = sqlx::query_as!( 305 + RecordRow, 306 + r#"SELECT rkey, record_cid FROM records 307 + WHERE repo_id = $1 AND collection = $2 AND rkey > $3 308 + ORDER BY rkey ASC LIMIT $4"#, 309 + repo_id, 310 + collection_str, 311 + cursor_str, 312 + limit 313 + ) 314 + .fetch_all(&self.pool) 315 + .await 316 + .map_err(map_sqlx_error)?; 317 + Ok(to_record_info(rows)) 318 + } 319 + }; 320 + } 321 + 322 + if let (Some(start), Some(end)) = (rkey_start, rkey_end) { 323 + let start_str = start.as_str(); 324 + let end_str = end.as_str(); 325 + return match reverse { 326 + false => { 327 + let rows = sqlx::query_as!( 328 + RecordRow, 329 + r#"SELECT rkey, record_cid FROM records 330 + WHERE repo_id = $1 AND collection = $2 AND rkey >= $3 AND rkey <= $4 331 + ORDER BY rkey DESC LIMIT $5"#, 332 + repo_id, 333 + collection_str, 334 + start_str, 335 + end_str, 336 + limit 337 + ) 338 + .fetch_all(&self.pool) 339 + .await 340 + .map_err(map_sqlx_error)?; 341 + Ok(to_record_info(rows)) 342 + } 343 + true => { 344 + let rows = sqlx::query_as!( 345 + RecordRow, 346 + r#"SELECT rkey, record_cid FROM records 347 + WHERE repo_id = $1 AND collection = $2 AND rkey >= $3 AND rkey <= $4 348 + ORDER BY rkey ASC LIMIT $5"#, 349 + repo_id, 350 + collection_str, 351 + start_str, 352 + end_str, 353 + limit 354 + ) 355 + .fetch_all(&self.pool) 356 + .await 357 + .map_err(map_sqlx_error)?; 358 + Ok(to_record_info(rows)) 359 + } 360 + }; 361 + } 362 + 363 + if let Some(start) = rkey_start { 364 + let start_str = start.as_str(); 365 + return match reverse { 366 + false => { 367 + let rows = sqlx::query_as!( 368 + RecordRow, 369 + r#"SELECT rkey, record_cid FROM records 370 + WHERE repo_id = $1 AND collection = $2 AND rkey >= $3 371 + ORDER BY rkey DESC LIMIT $4"#, 372 + repo_id, 373 + collection_str, 374 + start_str, 375 + limit 376 + ) 377 + .fetch_all(&self.pool) 378 + .await 379 + .map_err(map_sqlx_error)?; 380 + Ok(to_record_info(rows)) 381 + } 382 + true => { 383 + let rows = sqlx::query_as!( 384 + RecordRow, 385 + r#"SELECT rkey, record_cid FROM records 386 + WHERE repo_id = $1 AND collection = $2 AND rkey >= $3 387 + ORDER BY rkey ASC LIMIT $4"#, 388 + repo_id, 389 + collection_str, 390 + start_str, 391 + limit 392 + ) 393 + .fetch_all(&self.pool) 394 + .await 395 + .map_err(map_sqlx_error)?; 396 + Ok(to_record_info(rows)) 397 + } 398 + }; 399 + } 400 + 401 + if let Some(end) = rkey_end { 402 + let end_str = end.as_str(); 403 + return match reverse { 404 + false => { 405 + let rows = sqlx::query_as!( 406 + RecordRow, 407 + r#"SELECT rkey, record_cid FROM records 408 + WHERE repo_id = $1 AND collection = $2 AND rkey <= $3 409 + ORDER BY rkey DESC LIMIT $4"#, 410 + repo_id, 411 + collection_str, 412 + end_str, 413 + limit 414 + ) 415 + .fetch_all(&self.pool) 416 + .await 417 + .map_err(map_sqlx_error)?; 418 + Ok(to_record_info(rows)) 419 + } 420 + true => { 421 + let rows = sqlx::query_as!( 422 + RecordRow, 423 + r#"SELECT rkey, record_cid FROM records 424 + WHERE repo_id = $1 AND collection = $2 AND rkey <= $3 425 + ORDER BY rkey ASC LIMIT $4"#, 426 + repo_id, 427 + collection_str, 428 + end_str, 429 + limit 430 + ) 431 + .fetch_all(&self.pool) 432 + .await 433 + .map_err(map_sqlx_error)?; 434 + Ok(to_record_info(rows)) 435 + } 436 + }; 437 + } 438 + 439 + match reverse { 440 + false => { 441 + let rows = sqlx::query_as!( 442 + RecordRow, 443 + r#"SELECT rkey, record_cid FROM records 444 + WHERE repo_id = $1 AND collection = $2 445 + ORDER BY rkey DESC LIMIT $3"#, 446 + repo_id, 447 + collection_str, 448 + limit 449 + ) 450 + .fetch_all(&self.pool) 451 + .await 452 + .map_err(map_sqlx_error)?; 453 + Ok(to_record_info(rows)) 454 + } 455 + true => { 456 + let rows = sqlx::query_as!( 457 + RecordRow, 458 + r#"SELECT rkey, record_cid FROM records 459 + WHERE repo_id = $1 AND collection = $2 460 + ORDER BY rkey ASC LIMIT $3"#, 461 + repo_id, 462 + collection_str, 463 + limit 464 + ) 465 + .fetch_all(&self.pool) 466 + .await 467 + .map_err(map_sqlx_error)?; 468 + Ok(to_record_info(rows)) 469 + } 470 + } 471 + } 472 + 473 + async fn get_all_records(&self, repo_id: Uuid) -> Result<Vec<FullRecordInfo>, DbError> { 474 + let rows = sqlx::query!( 475 + "SELECT collection, rkey, record_cid FROM records WHERE repo_id = $1", 476 + repo_id 477 + ) 478 + .fetch_all(&self.pool) 479 + .await 480 + .map_err(map_sqlx_error)?; 481 + 482 + Ok(rows 483 + .into_iter() 484 + .map(|r| FullRecordInfo { 485 + collection: Nsid::from(r.collection), 486 + rkey: Rkey::from(r.rkey), 487 + record_cid: CidLink::from(r.record_cid), 488 + }) 489 + .collect()) 490 + } 491 + 492 + async fn list_collections(&self, repo_id: Uuid) -> Result<Vec<Nsid>, DbError> { 493 + let rows = sqlx::query_scalar!( 494 + "SELECT DISTINCT collection FROM records WHERE repo_id = $1", 495 + repo_id 496 + ) 497 + .fetch_all(&self.pool) 498 + .await 499 + .map_err(map_sqlx_error)?; 500 + 501 + Ok(rows.into_iter().map(Nsid::from).collect()) 502 + } 503 + 504 + async fn count_records(&self, repo_id: Uuid) -> Result<i64, DbError> { 505 + let count = sqlx::query_scalar!( 506 + r#"SELECT COUNT(*) as "count!" FROM records WHERE repo_id = $1"#, 507 + repo_id 508 + ) 509 + .fetch_one(&self.pool) 510 + .await 511 + .map_err(map_sqlx_error)?; 512 + 513 + Ok(count) 514 + } 515 + 516 + async fn count_all_records(&self) -> Result<i64, DbError> { 517 + let count = sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!" FROM records"#) 518 + .fetch_one(&self.pool) 519 + .await 520 + .map_err(map_sqlx_error)?; 521 + 522 + Ok(count) 523 + } 524 + 525 + async fn get_record_by_cid(&self, cid: &CidLink) -> Result<Option<RecordWithTakedown>, DbError> { 526 + let row = sqlx::query!( 527 + "SELECT id, takedown_ref FROM records WHERE record_cid = $1", 528 + cid.as_str() 529 + ) 530 + .fetch_optional(&self.pool) 531 + .await 532 + .map_err(map_sqlx_error)?; 533 + 534 + Ok(row.map(|r| RecordWithTakedown { 535 + id: r.id, 536 + takedown_ref: r.takedown_ref, 537 + })) 538 + } 539 + 540 + async fn set_record_takedown( 541 + &self, 542 + cid: &CidLink, 543 + takedown_ref: Option<&str>, 544 + ) -> Result<(), DbError> { 545 + sqlx::query!( 546 + "UPDATE records SET takedown_ref = $1 WHERE record_cid = $2", 547 + takedown_ref, 548 + cid.as_str() 549 + ) 550 + .execute(&self.pool) 551 + .await 552 + .map_err(map_sqlx_error)?; 553 + 554 + Ok(()) 555 + } 556 + 557 + async fn insert_user_blocks( 558 + &self, 559 + user_id: Uuid, 560 + block_cids: &[Vec<u8>], 561 + repo_rev: &str, 562 + ) -> Result<(), DbError> { 563 + sqlx::query( 564 + r#" 565 + INSERT INTO user_blocks (user_id, block_cid, repo_rev) 566 + SELECT $1, block_cid, $3 FROM UNNEST($2::bytea[]) AS t(block_cid) 567 + ON CONFLICT (user_id, block_cid) DO NOTHING 568 + "#, 569 + ) 570 + .bind(user_id) 571 + .bind(block_cids) 572 + .bind(repo_rev) 573 + .execute(&self.pool) 574 + .await 575 + .map_err(map_sqlx_error)?; 576 + 577 + Ok(()) 578 + } 579 + 580 + async fn delete_user_blocks( 581 + &self, 582 + user_id: Uuid, 583 + block_cids: &[Vec<u8>], 584 + ) -> Result<(), DbError> { 585 + sqlx::query!( 586 + "DELETE FROM user_blocks WHERE user_id = $1 AND block_cid = ANY($2)", 587 + user_id, 588 + block_cids 589 + ) 590 + .execute(&self.pool) 591 + .await 592 + .map_err(map_sqlx_error)?; 593 + 594 + Ok(()) 595 + } 596 + 597 + async fn count_user_blocks(&self, user_id: Uuid) -> Result<i64, DbError> { 598 + let count = sqlx::query_scalar!( 599 + r#"SELECT COUNT(*) as "count!" FROM user_blocks WHERE user_id = $1"#, 600 + user_id 601 + ) 602 + .fetch_one(&self.pool) 603 + .await 604 + .map_err(map_sqlx_error)?; 605 + 606 + Ok(count) 607 + } 608 + 609 + async fn get_user_block_cids_since_rev( 610 + &self, 611 + user_id: Uuid, 612 + since_rev: &str, 613 + ) -> Result<Vec<Vec<u8>>, DbError> { 614 + let rows: Vec<(Vec<u8>,)> = sqlx::query_as( 615 + r#" 616 + SELECT block_cid FROM user_blocks 617 + WHERE user_id = $1 AND repo_rev > $2 618 + ORDER BY repo_rev ASC 619 + "#, 620 + ) 621 + .bind(user_id) 622 + .bind(since_rev) 623 + .fetch_all(&self.pool) 624 + .await 625 + .map_err(map_sqlx_error)?; 626 + 627 + Ok(rows.into_iter().map(|(cid,)| cid).collect()) 628 + } 629 + 630 + async fn insert_commit_event(&self, data: &CommitEventData) -> Result<i64, DbError> { 631 + let seq = sqlx::query_scalar!( 632 + r#" 633 + INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, prev_data_cid, rev) 634 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 635 + RETURNING seq 636 + "#, 637 + data.did.as_str(), 638 + data.event_type, 639 + data.commit_cid.as_ref().map(|c| c.as_str()), 640 + data.prev_cid.as_ref().map(|c| c.as_str()), 641 + data.ops, 642 + data.blobs.as_deref(), 643 + data.blocks_cids.as_deref(), 644 + data.prev_data_cid.as_ref().map(|c| c.as_str()), 645 + data.rev 646 + ) 647 + .fetch_one(&self.pool) 648 + .await 649 + .map_err(map_sqlx_error)?; 650 + 651 + Ok(seq) 652 + } 653 + 654 + async fn insert_identity_event(&self, did: &Did, handle: Option<&Handle>) -> Result<i64, DbError> { 655 + let handle_str = handle.map(|h| h.as_str()); 656 + let seq = sqlx::query_scalar!( 657 + r#" 658 + INSERT INTO repo_seq (did, event_type, handle) 659 + VALUES ($1, 'identity', $2) 660 + RETURNING seq 661 + "#, 662 + did.as_str(), 663 + handle_str 664 + ) 665 + .fetch_one(&self.pool) 666 + .await 667 + .map_err(map_sqlx_error)?; 668 + 669 + sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq)) 670 + .execute(&self.pool) 671 + .await 672 + .map_err(map_sqlx_error)?; 673 + 674 + Ok(seq) 675 + } 676 + 677 + async fn insert_account_event( 678 + &self, 679 + did: &Did, 680 + active: bool, 681 + status: Option<&str>, 682 + ) -> Result<i64, DbError> { 683 + let seq = sqlx::query_scalar!( 684 + r#" 685 + INSERT INTO repo_seq (did, event_type, active, status) 686 + VALUES ($1, 'account', $2, $3) 687 + RETURNING seq 688 + "#, 689 + did.as_str(), 690 + active, 691 + status 692 + ) 693 + .fetch_one(&self.pool) 694 + .await 695 + .map_err(map_sqlx_error)?; 696 + 697 + sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq)) 698 + .execute(&self.pool) 699 + .await 700 + .map_err(map_sqlx_error)?; 701 + 702 + Ok(seq) 703 + } 704 + 705 + async fn insert_sync_event( 706 + &self, 707 + did: &Did, 708 + commit_cid: &CidLink, 709 + rev: Option<&str>, 710 + ) -> Result<i64, DbError> { 711 + let seq = sqlx::query_scalar!( 712 + r#" 713 + INSERT INTO repo_seq (did, event_type, commit_cid, rev) 714 + VALUES ($1, 'sync', $2, $3) 715 + RETURNING seq 716 + "#, 717 + did.as_str(), 718 + commit_cid.as_str(), 719 + rev 720 + ) 721 + .fetch_one(&self.pool) 722 + .await 723 + .map_err(map_sqlx_error)?; 724 + 725 + sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq)) 726 + .execute(&self.pool) 727 + .await 728 + .map_err(map_sqlx_error)?; 729 + 730 + Ok(seq) 731 + } 732 + 733 + async fn insert_genesis_commit_event( 734 + &self, 735 + did: &Did, 736 + commit_cid: &CidLink, 737 + mst_root_cid: &CidLink, 738 + rev: &str, 739 + ) -> Result<i64, DbError> { 740 + let ops = serde_json::json!([]); 741 + let blobs: Vec<String> = vec![]; 742 + let blocks_cids: Vec<String> = vec![mst_root_cid.to_string(), commit_cid.to_string()]; 743 + let prev_cid: Option<&str> = None; 744 + 745 + let seq = sqlx::query_scalar!( 746 + r#" 747 + INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, rev) 748 + VALUES ($1, 'commit', $2, $3::TEXT, $4, $5, $6, $7) 749 + RETURNING seq 750 + "#, 751 + did.as_str(), 752 + commit_cid.as_str(), 753 + prev_cid, 754 + ops, 755 + &blobs, 756 + &blocks_cids, 757 + rev 758 + ) 759 + .fetch_one(&self.pool) 760 + .await 761 + .map_err(map_sqlx_error)?; 762 + 763 + sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq)) 764 + .execute(&self.pool) 765 + .await 766 + .map_err(map_sqlx_error)?; 767 + 768 + Ok(seq) 769 + } 770 + 771 + async fn update_seq_blocks_cids( 772 + &self, 773 + seq: i64, 774 + blocks_cids: &[String], 775 + ) -> Result<(), DbError> { 776 + sqlx::query!( 777 + "UPDATE repo_seq SET blocks_cids = $1 WHERE seq = $2", 778 + blocks_cids, 779 + seq 780 + ) 781 + .execute(&self.pool) 782 + .await 783 + .map_err(map_sqlx_error)?; 784 + 785 + Ok(()) 786 + } 787 + 788 + async fn delete_sequences_except(&self, did: &Did, keep_seq: i64) -> Result<(), DbError> { 789 + sqlx::query!( 790 + "DELETE FROM repo_seq WHERE did = $1 AND seq != $2", 791 + did.as_str(), 792 + keep_seq 793 + ) 794 + .execute(&self.pool) 795 + .await 796 + .map_err(map_sqlx_error)?; 797 + 798 + Ok(()) 799 + } 800 + 801 + async fn get_max_seq(&self) -> Result<i64, DbError> { 802 + let seq = sqlx::query_scalar!(r#"SELECT COALESCE(MAX(seq), 0) as "max!" FROM repo_seq"#) 803 + .fetch_one(&self.pool) 804 + .await 805 + .map_err(map_sqlx_error)?; 806 + 807 + Ok(seq) 808 + } 809 + 810 + async fn get_min_seq_since(&self, since: DateTime<Utc>) -> Result<Option<i64>, DbError> { 811 + let seq = sqlx::query_scalar!( 812 + "SELECT MIN(seq) FROM repo_seq WHERE created_at >= $1", 813 + since 814 + ) 815 + .fetch_one(&self.pool) 816 + .await 817 + .map_err(map_sqlx_error)?; 818 + 819 + Ok(seq) 820 + } 821 + 822 + async fn get_account_with_repo(&self, did: &Did) -> Result<Option<RepoAccountInfo>, DbError> { 823 + let row = sqlx::query!( 824 + r#"SELECT u.id, u.did, u.deactivated_at, u.takedown_ref, r.repo_root_cid as "repo_root_cid?" 825 + FROM users u 826 + LEFT JOIN repos r ON r.user_id = u.id 827 + WHERE u.did = $1"#, 828 + did.as_str() 829 + ) 830 + .fetch_optional(&self.pool) 831 + .await 832 + .map_err(map_sqlx_error)?; 833 + 834 + Ok(row.map(|r| RepoAccountInfo { 835 + user_id: r.id, 836 + did: Did::from(r.did), 837 + deactivated_at: r.deactivated_at, 838 + takedown_ref: r.takedown_ref, 839 + repo_root_cid: r.repo_root_cid.map(CidLink::from), 840 + })) 841 + } 842 + 843 + async fn get_events_since_seq( 844 + &self, 845 + since_seq: i64, 846 + limit: Option<i64>, 847 + ) -> Result<Vec<SequencedEvent>, DbError> { 848 + let map_row = |r: SequencedEventRow| SequencedEvent { 849 + seq: r.seq, 850 + did: Did::from(r.did), 851 + created_at: r.created_at, 852 + event_type: r.event_type, 853 + commit_cid: r.commit_cid.map(CidLink::from), 854 + prev_cid: r.prev_cid.map(CidLink::from), 855 + prev_data_cid: r.prev_data_cid.map(CidLink::from), 856 + ops: r.ops, 857 + blobs: r.blobs, 858 + blocks_cids: r.blocks_cids, 859 + handle: r.handle.map(Handle::from), 860 + active: r.active, 861 + status: r.status, 862 + rev: r.rev, 863 + }; 864 + match limit { 865 + Some(lim) => { 866 + let rows = sqlx::query_as!( 867 + SequencedEventRow, 868 + r#"SELECT seq, did, created_at, event_type, commit_cid, prev_cid, prev_data_cid, 869 + ops, blobs, blocks_cids, handle, active, status, rev 870 + FROM repo_seq 871 + WHERE seq > $1 872 + ORDER BY seq ASC 873 + LIMIT $2"#, 874 + since_seq, 875 + lim 876 + ) 877 + .fetch_all(&self.pool) 878 + .await 879 + .map_err(map_sqlx_error)?; 880 + Ok(rows.into_iter().map(map_row).collect()) 881 + } 882 + None => { 883 + let rows = sqlx::query_as!( 884 + SequencedEventRow, 885 + r#"SELECT seq, did, created_at, event_type, commit_cid, prev_cid, prev_data_cid, 886 + ops, blobs, blocks_cids, handle, active, status, rev 887 + FROM repo_seq 888 + WHERE seq > $1 889 + ORDER BY seq ASC"#, 890 + since_seq 891 + ) 892 + .fetch_all(&self.pool) 893 + .await 894 + .map_err(map_sqlx_error)?; 895 + Ok(rows.into_iter().map(map_row).collect()) 896 + } 897 + } 898 + } 899 + 900 + async fn get_events_in_seq_range( 901 + &self, 902 + start_seq: i64, 903 + end_seq: i64, 904 + ) -> Result<Vec<SequencedEvent>, DbError> { 905 + let rows = sqlx::query!( 906 + r#"SELECT seq, did, created_at, event_type, commit_cid, prev_cid, prev_data_cid, 907 + ops, blobs, blocks_cids, handle, active, status, rev 908 + FROM repo_seq 909 + WHERE seq > $1 AND seq < $2 910 + ORDER BY seq ASC"#, 911 + start_seq, 912 + end_seq 913 + ) 914 + .fetch_all(&self.pool) 915 + .await 916 + .map_err(map_sqlx_error)?; 917 + Ok(rows 918 + .into_iter() 919 + .map(|r| SequencedEvent { 920 + seq: r.seq, 921 + did: Did::from(r.did), 922 + created_at: r.created_at, 923 + event_type: r.event_type, 924 + commit_cid: r.commit_cid.map(CidLink::from), 925 + prev_cid: r.prev_cid.map(CidLink::from), 926 + prev_data_cid: r.prev_data_cid.map(CidLink::from), 927 + ops: r.ops, 928 + blobs: r.blobs, 929 + blocks_cids: r.blocks_cids, 930 + handle: r.handle.map(Handle::from), 931 + active: r.active, 932 + status: r.status, 933 + rev: r.rev, 934 + }) 935 + .collect()) 936 + } 937 + 938 + async fn get_event_by_seq(&self, seq: i64) -> Result<Option<SequencedEvent>, DbError> { 939 + let row = sqlx::query!( 940 + r#"SELECT seq, did, created_at, event_type, commit_cid, prev_cid, prev_data_cid, 941 + ops, blobs, blocks_cids, handle, active, status, rev 942 + FROM repo_seq 943 + WHERE seq = $1"#, 944 + seq 945 + ) 946 + .fetch_optional(&self.pool) 947 + .await 948 + .map_err(map_sqlx_error)?; 949 + Ok(row.map(|r| SequencedEvent { 950 + seq: r.seq, 951 + did: Did::from(r.did), 952 + created_at: r.created_at, 953 + event_type: r.event_type, 954 + commit_cid: r.commit_cid.map(CidLink::from), 955 + prev_cid: r.prev_cid.map(CidLink::from), 956 + prev_data_cid: r.prev_data_cid.map(CidLink::from), 957 + ops: r.ops, 958 + blobs: r.blobs, 959 + blocks_cids: r.blocks_cids, 960 + handle: r.handle.map(Handle::from), 961 + active: r.active, 962 + status: r.status, 963 + rev: r.rev, 964 + })) 965 + } 966 + 967 + async fn get_events_since_cursor( 968 + &self, 969 + cursor: i64, 970 + limit: i64, 971 + ) -> Result<Vec<SequencedEvent>, DbError> { 972 + let rows = sqlx::query!( 973 + r#"SELECT seq, did, created_at, event_type, commit_cid, prev_cid, prev_data_cid, 974 + ops, blobs, blocks_cids, handle, active, status, rev 975 + FROM repo_seq 976 + WHERE seq > $1 977 + ORDER BY seq ASC 978 + LIMIT $2"#, 979 + cursor, 980 + limit 981 + ) 982 + .fetch_all(&self.pool) 983 + .await 984 + .map_err(map_sqlx_error)?; 985 + Ok(rows 986 + .into_iter() 987 + .map(|r| SequencedEvent { 988 + seq: r.seq, 989 + did: Did::from(r.did), 990 + created_at: r.created_at, 991 + event_type: r.event_type, 992 + commit_cid: r.commit_cid.map(CidLink::from), 993 + prev_cid: r.prev_cid.map(CidLink::from), 994 + prev_data_cid: r.prev_data_cid.map(CidLink::from), 995 + ops: r.ops, 996 + blobs: r.blobs, 997 + blocks_cids: r.blocks_cids, 998 + handle: r.handle.map(Handle::from), 999 + active: r.active, 1000 + status: r.status, 1001 + rev: r.rev, 1002 + }) 1003 + .collect()) 1004 + } 1005 + 1006 + async fn get_events_since_rev( 1007 + &self, 1008 + did: &Did, 1009 + since_rev: &str, 1010 + ) -> Result<Vec<EventBlocksCids>, DbError> { 1011 + let rows = sqlx::query!( 1012 + r#"SELECT blocks_cids, commit_cid 1013 + FROM repo_seq 1014 + WHERE did = $1 AND rev > $2 1015 + ORDER BY seq DESC"#, 1016 + did.as_str(), 1017 + since_rev 1018 + ) 1019 + .fetch_all(&self.pool) 1020 + .await 1021 + .map_err(map_sqlx_error)?; 1022 + 1023 + Ok(rows 1024 + .into_iter() 1025 + .map(|r| EventBlocksCids { 1026 + blocks_cids: r.blocks_cids, 1027 + commit_cid: r.commit_cid.map(CidLink::from), 1028 + }) 1029 + .collect()) 1030 + } 1031 + 1032 + async fn list_repos_paginated( 1033 + &self, 1034 + cursor_did: Option<&Did>, 1035 + limit: i64, 1036 + ) -> Result<Vec<RepoListItem>, DbError> { 1037 + let cursor_str = cursor_did.map(|d| d.as_str()).unwrap_or(""); 1038 + let rows = sqlx::query!( 1039 + r#"SELECT u.did, u.deactivated_at, u.takedown_ref, r.repo_root_cid, r.repo_rev 1040 + FROM repos r 1041 + JOIN users u ON r.user_id = u.id 1042 + WHERE u.did > $1 1043 + ORDER BY u.did ASC 1044 + LIMIT $2"#, 1045 + cursor_str, 1046 + limit 1047 + ) 1048 + .fetch_all(&self.pool) 1049 + .await 1050 + .map_err(map_sqlx_error)?; 1051 + 1052 + Ok(rows 1053 + .into_iter() 1054 + .map(|r| RepoListItem { 1055 + did: Did::from(r.did), 1056 + deactivated_at: r.deactivated_at, 1057 + takedown_ref: r.takedown_ref, 1058 + repo_root_cid: CidLink::from(r.repo_root_cid), 1059 + repo_rev: r.repo_rev, 1060 + }) 1061 + .collect()) 1062 + } 1063 + 1064 + async fn get_repo_root_cid_by_user_id(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError> { 1065 + let cid = sqlx::query_scalar!( 1066 + "SELECT repo_root_cid FROM repos WHERE user_id = $1", 1067 + user_id 1068 + ) 1069 + .fetch_optional(&self.pool) 1070 + .await 1071 + .map_err(map_sqlx_error)?; 1072 + Ok(cid.map(CidLink::from)) 1073 + } 1074 + 1075 + async fn notify_update(&self, seq: i64) -> Result<(), DbError> { 1076 + sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq)) 1077 + .execute(&self.pool) 1078 + .await 1079 + .map_err(map_sqlx_error)?; 1080 + Ok(()) 1081 + } 1082 + 1083 + async fn import_repo_data( 1084 + &self, 1085 + user_id: Uuid, 1086 + blocks: &[ImportBlock], 1087 + records: &[ImportRecord], 1088 + ) -> Result<(), ImportRepoError> { 1089 + let mut tx = self 1090 + .pool 1091 + .begin() 1092 + .await 1093 + .map_err(|e| ImportRepoError::Database(e.to_string()))?; 1094 + 1095 + let repo = sqlx::query!( 1096 + "SELECT repo_root_cid FROM repos WHERE user_id = $1 FOR UPDATE NOWAIT", 1097 + user_id 1098 + ) 1099 + .fetch_optional(&mut *tx) 1100 + .await 1101 + .map_err(|e| { 1102 + if let sqlx::Error::Database(ref db_err) = e 1103 + && db_err.code().as_deref() == Some("55P03") 1104 + { 1105 + return ImportRepoError::ConcurrentModification; 1106 + } 1107 + ImportRepoError::Database(e.to_string()) 1108 + })?; 1109 + 1110 + if repo.is_none() { 1111 + return Err(ImportRepoError::RepoNotFound); 1112 + } 1113 + 1114 + let block_chunks: Vec<Vec<&ImportBlock>> = blocks 1115 + .iter() 1116 + .collect::<Vec<_>>() 1117 + .chunks(100) 1118 + .map(|c| c.to_vec()) 1119 + .collect(); 1120 + 1121 + for chunk in block_chunks { 1122 + for block in chunk { 1123 + sqlx::query!( 1124 + "INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING", 1125 + &block.cid_bytes, 1126 + &block.data 1127 + ) 1128 + .execute(&mut *tx) 1129 + .await 1130 + .map_err(|e| ImportRepoError::Database(e.to_string()))?; 1131 + } 1132 + } 1133 + 1134 + sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id) 1135 + .execute(&mut *tx) 1136 + .await 1137 + .map_err(|e| ImportRepoError::Database(e.to_string()))?; 1138 + 1139 + for record in records { 1140 + sqlx::query!( 1141 + r#" 1142 + INSERT INTO records (repo_id, collection, rkey, record_cid) 1143 + VALUES ($1, $2, $3, $4) 1144 + ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4 1145 + "#, 1146 + user_id, 1147 + record.collection.as_str(), 1148 + record.rkey.as_str(), 1149 + record.record_cid.as_str() 1150 + ) 1151 + .execute(&mut *tx) 1152 + .await 1153 + .map_err(|e| ImportRepoError::Database(e.to_string()))?; 1154 + } 1155 + 1156 + tx.commit() 1157 + .await 1158 + .map_err(|e| ImportRepoError::Database(e.to_string()))?; 1159 + 1160 + Ok(()) 1161 + } 1162 + 1163 + async fn apply_commit( 1164 + &self, 1165 + input: tranquil_db_traits::ApplyCommitInput, 1166 + ) -> Result<tranquil_db_traits::ApplyCommitResult, tranquil_db_traits::ApplyCommitError> { 1167 + use tranquil_db_traits::ApplyCommitError; 1168 + 1169 + let mut tx = self 1170 + .pool 1171 + .begin() 1172 + .await 1173 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1174 + 1175 + let lock_result: Result<Option<_>, sqlx::Error> = sqlx::query!( 1176 + "SELECT repo_root_cid FROM repos WHERE user_id = $1 FOR UPDATE NOWAIT", 1177 + input.user_id 1178 + ) 1179 + .fetch_optional(&mut *tx) 1180 + .await; 1181 + 1182 + match lock_result { 1183 + Err(e) => { 1184 + if let Some(db_err) = e.as_database_error() 1185 + && db_err.code().as_deref() == Some("55P03") 1186 + { 1187 + return Err(ApplyCommitError::ConcurrentModification); 1188 + } 1189 + return Err(ApplyCommitError::Database(format!( 1190 + "Failed to acquire repo lock: {}", 1191 + e 1192 + ))); 1193 + } 1194 + Ok(Some(row)) => { 1195 + if let Some(expected_root) = &input.expected_root_cid 1196 + && row.repo_root_cid != expected_root.as_str() 1197 + { 1198 + return Err(ApplyCommitError::ConcurrentModification); 1199 + } 1200 + } 1201 + Ok(None) => { 1202 + return Err(ApplyCommitError::RepoNotFound); 1203 + } 1204 + } 1205 + 1206 + let is_account_active: bool = sqlx::query_scalar( 1207 + "SELECT deactivated_at IS NULL FROM users WHERE id = $1", 1208 + ) 1209 + .bind(input.user_id) 1210 + .fetch_optional(&mut *tx) 1211 + .await 1212 + .map_err(|e| ApplyCommitError::Database(e.to_string()))? 1213 + .flatten() 1214 + .unwrap_or(false); 1215 + 1216 + sqlx::query( 1217 + "UPDATE repos SET repo_root_cid = $1, repo_rev = $2 WHERE user_id = $3", 1218 + ) 1219 + .bind(&input.new_root_cid) 1220 + .bind(&input.new_rev) 1221 + .bind(input.user_id) 1222 + .execute(&mut *tx) 1223 + .await 1224 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1225 + 1226 + if !input.new_block_cids.is_empty() { 1227 + sqlx::query( 1228 + r#" 1229 + INSERT INTO user_blocks (user_id, block_cid, repo_rev) 1230 + SELECT $1, block_cid, $3 FROM UNNEST($2::bytea[]) AS t(block_cid) 1231 + ON CONFLICT (user_id, block_cid) DO NOTHING 1232 + "#, 1233 + ) 1234 + .bind(input.user_id) 1235 + .bind(&input.new_block_cids) 1236 + .bind(&input.new_rev) 1237 + .execute(&mut *tx) 1238 + .await 1239 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1240 + } 1241 + 1242 + if !input.obsolete_block_cids.is_empty() { 1243 + sqlx::query( 1244 + r#" 1245 + DELETE FROM user_blocks 1246 + WHERE user_id = $1 1247 + AND block_cid = ANY($2) 1248 + "#, 1249 + ) 1250 + .bind(input.user_id) 1251 + .bind(&input.obsolete_block_cids) 1252 + .execute(&mut *tx) 1253 + .await 1254 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1255 + } 1256 + 1257 + if !input.record_upserts.is_empty() { 1258 + let collections: Vec<&str> = input 1259 + .record_upserts 1260 + .iter() 1261 + .map(|r| r.collection.as_str()) 1262 + .collect(); 1263 + let rkeys: Vec<&str> = input.record_upserts.iter().map(|r| r.rkey.as_str()).collect(); 1264 + let cids: Vec<&str> = input.record_upserts.iter().map(|r| r.cid.as_str()).collect(); 1265 + 1266 + sqlx::query( 1267 + r#" 1268 + INSERT INTO records (repo_id, collection, rkey, record_cid, repo_rev) 1269 + SELECT $1, t.collection, t.rkey, t.cid, $5 1270 + FROM UNNEST($2::text[], $3::text[], $4::text[]) AS t(collection, rkey, cid) 1271 + ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = EXCLUDED.record_cid, repo_rev = EXCLUDED.repo_rev 1272 + "#, 1273 + ) 1274 + .bind(input.user_id) 1275 + .bind(&collections) 1276 + .bind(&rkeys) 1277 + .bind(&cids) 1278 + .bind(&input.new_rev) 1279 + .execute(&mut *tx) 1280 + .await 1281 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1282 + } 1283 + 1284 + if !input.record_deletes.is_empty() { 1285 + let collections: Vec<&str> = input 1286 + .record_deletes 1287 + .iter() 1288 + .map(|r| r.collection.as_str()) 1289 + .collect(); 1290 + let rkeys: Vec<&str> = input.record_deletes.iter().map(|r| r.rkey.as_str()).collect(); 1291 + 1292 + sqlx::query( 1293 + r#" 1294 + DELETE FROM records 1295 + WHERE repo_id = $1 1296 + AND (collection, rkey) IN (SELECT collection, rkey FROM UNNEST($2::text[], $3::text[]) AS t(collection, rkey)) 1297 + "#, 1298 + ) 1299 + .bind(input.user_id) 1300 + .bind(&collections) 1301 + .bind(&rkeys) 1302 + .execute(&mut *tx) 1303 + .await 1304 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1305 + } 1306 + 1307 + let event = &input.commit_event; 1308 + let seq: i64 = sqlx::query_scalar( 1309 + r#" 1310 + INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, prev_data_cid, rev) 1311 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 1312 + RETURNING seq 1313 + "#, 1314 + ) 1315 + .bind(&event.did) 1316 + .bind(&event.event_type) 1317 + .bind(&event.commit_cid) 1318 + .bind(&event.prev_cid) 1319 + .bind(&event.ops) 1320 + .bind(&event.blobs) 1321 + .bind(&event.blocks_cids) 1322 + .bind(&event.prev_data_cid) 1323 + .bind(&event.rev) 1324 + .fetch_one(&mut *tx) 1325 + .await 1326 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1327 + 1328 + sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq)) 1329 + .execute(&mut *tx) 1330 + .await 1331 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1332 + 1333 + tx.commit() 1334 + .await 1335 + .map_err(|e| ApplyCommitError::Database(e.to_string()))?; 1336 + 1337 + Ok(tranquil_db_traits::ApplyCommitResult { 1338 + seq, 1339 + is_account_active, 1340 + }) 1341 + } 1342 + 1343 + async fn get_broken_genesis_commits( 1344 + &self, 1345 + ) -> Result<Vec<tranquil_db_traits::BrokenGenesisCommit>, DbError> { 1346 + let rows = sqlx::query!( 1347 + r#" 1348 + SELECT seq, did, commit_cid 1349 + FROM repo_seq 1350 + WHERE event_type = 'commit' 1351 + AND prev_cid IS NULL 1352 + AND (blocks_cids IS NULL OR array_length(blocks_cids, 1) IS NULL OR array_length(blocks_cids, 1) = 0) 1353 + "# 1354 + ) 1355 + .fetch_all(&self.pool) 1356 + .await 1357 + .map_err(map_sqlx_error)?; 1358 + 1359 + Ok(rows 1360 + .into_iter() 1361 + .map(|r| BrokenGenesisCommit { 1362 + seq: r.seq, 1363 + did: Did::from(r.did), 1364 + commit_cid: r.commit_cid.map(CidLink::from), 1365 + }) 1366 + .collect()) 1367 + } 1368 + 1369 + async fn get_users_without_blocks( 1370 + &self, 1371 + ) -> Result<Vec<UserWithoutBlocks>, DbError> { 1372 + let rows: Vec<(Uuid, String, Option<String>)> = sqlx::query_as( 1373 + r#" 1374 + SELECT u.id as user_id, r.repo_root_cid, r.repo_rev 1375 + FROM users u 1376 + JOIN repos r ON r.user_id = u.id 1377 + WHERE NOT EXISTS (SELECT 1 FROM user_blocks ub WHERE ub.user_id = u.id) 1378 + "#, 1379 + ) 1380 + .fetch_all(&self.pool) 1381 + .await 1382 + .map_err(map_sqlx_error)?; 1383 + 1384 + Ok(rows 1385 + .into_iter() 1386 + .map(|(user_id, repo_root_cid, repo_rev)| UserWithoutBlocks { 1387 + user_id, 1388 + repo_root_cid: CidLink::from(repo_root_cid), 1389 + repo_rev, 1390 + }) 1391 + .collect()) 1392 + } 1393 + 1394 + async fn get_users_needing_record_blobs_backfill( 1395 + &self, 1396 + limit: i64, 1397 + ) -> Result<Vec<tranquil_db_traits::UserNeedingRecordBlobsBackfill>, DbError> { 1398 + let rows = sqlx::query!( 1399 + r#" 1400 + SELECT DISTINCT u.id as user_id, u.did 1401 + FROM users u 1402 + JOIN records r ON r.repo_id = u.id 1403 + WHERE NOT EXISTS (SELECT 1 FROM record_blobs rb WHERE rb.repo_id = u.id) 1404 + LIMIT $1 1405 + "#, 1406 + limit 1407 + ) 1408 + .fetch_all(&self.pool) 1409 + .await 1410 + .map_err(map_sqlx_error)?; 1411 + 1412 + Ok(rows 1413 + .into_iter() 1414 + .map(|r| UserNeedingRecordBlobsBackfill { 1415 + user_id: r.user_id, 1416 + did: Did::from(r.did), 1417 + }) 1418 + .collect()) 1419 + } 1420 + 1421 + async fn insert_record_blobs( 1422 + &self, 1423 + repo_id: Uuid, 1424 + record_uris: &[AtUri], 1425 + blob_cids: &[CidLink], 1426 + ) -> Result<(), DbError> { 1427 + let uris_str: Vec<&str> = record_uris.iter().map(|u| u.as_str()).collect(); 1428 + let cids_str: Vec<&str> = blob_cids.iter().map(|c| c.as_str()).collect(); 1429 + 1430 + sqlx::query!( 1431 + r#" 1432 + INSERT INTO record_blobs (repo_id, record_uri, blob_cid) 1433 + SELECT $1, record_uri, blob_cid 1434 + FROM UNNEST($2::text[], $3::text[]) AS t(record_uri, blob_cid) 1435 + ON CONFLICT (repo_id, record_uri, blob_cid) DO NOTHING 1436 + "#, 1437 + repo_id, 1438 + &uris_str as &[&str], 1439 + &cids_str as &[&str] 1440 + ) 1441 + .execute(&self.pool) 1442 + .await 1443 + .map_err(map_sqlx_error)?; 1444 + 1445 + Ok(()) 1446 + } 1447 + }
+567
crates/tranquil-db/src/postgres/session.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use sqlx::PgPool; 4 + use tranquil_db_traits::{ 5 + AppPasswordCreate, AppPasswordRecord, DbError, RefreshSessionResult, SessionForRefresh, 6 + SessionListItem, SessionMfaStatus, SessionRefreshData, SessionRepository, SessionToken, 7 + SessionTokenCreate, 8 + }; 9 + use tranquil_types::Did; 10 + use uuid::Uuid; 11 + 12 + use super::user::map_sqlx_error; 13 + 14 + pub struct PostgresSessionRepository { 15 + pool: PgPool, 16 + } 17 + 18 + impl PostgresSessionRepository { 19 + pub fn new(pool: PgPool) -> Self { 20 + Self { pool } 21 + } 22 + } 23 + 24 + #[async_trait] 25 + impl SessionRepository for PostgresSessionRepository { 26 + async fn create_session(&self, data: &SessionTokenCreate) -> Result<i32, DbError> { 27 + let row = sqlx::query!( 28 + r#" 29 + INSERT INTO session_tokens 30 + (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, 31 + legacy_login, mfa_verified, scope, controller_did, app_password_name) 32 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 33 + RETURNING id 34 + "#, 35 + data.did.as_str(), 36 + data.access_jti, 37 + data.refresh_jti, 38 + data.access_expires_at, 39 + data.refresh_expires_at, 40 + data.legacy_login, 41 + data.mfa_verified, 42 + data.scope, 43 + data.controller_did.as_ref().map(|d| d.as_str()), 44 + data.app_password_name 45 + ) 46 + .fetch_one(&self.pool) 47 + .await 48 + .map_err(map_sqlx_error)?; 49 + 50 + Ok(row.id) 51 + } 52 + 53 + async fn get_session_by_access_jti( 54 + &self, 55 + access_jti: &str, 56 + ) -> Result<Option<SessionToken>, DbError> { 57 + let row = sqlx::query!( 58 + r#" 59 + SELECT id, did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, 60 + legacy_login, mfa_verified, scope, controller_did, app_password_name, 61 + created_at, updated_at 62 + FROM session_tokens 63 + WHERE access_jti = $1 64 + "#, 65 + access_jti 66 + ) 67 + .fetch_optional(&self.pool) 68 + .await 69 + .map_err(map_sqlx_error)?; 70 + 71 + Ok(row.map(|r| SessionToken { 72 + id: r.id, 73 + did: Did::from(r.did), 74 + access_jti: r.access_jti, 75 + refresh_jti: r.refresh_jti, 76 + access_expires_at: r.access_expires_at, 77 + refresh_expires_at: r.refresh_expires_at, 78 + legacy_login: r.legacy_login, 79 + mfa_verified: r.mfa_verified, 80 + scope: r.scope, 81 + controller_did: r.controller_did.map(Did::from), 82 + app_password_name: r.app_password_name, 83 + created_at: r.created_at, 84 + updated_at: r.updated_at, 85 + })) 86 + } 87 + 88 + async fn get_session_for_refresh( 89 + &self, 90 + refresh_jti: &str, 91 + ) -> Result<Option<SessionForRefresh>, DbError> { 92 + let row = sqlx::query!( 93 + r#" 94 + SELECT st.id, st.did, st.scope, st.controller_did, k.key_bytes, k.encryption_version 95 + FROM session_tokens st 96 + JOIN users u ON st.did = u.did 97 + JOIN user_keys k ON u.id = k.user_id 98 + WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW() 99 + "#, 100 + refresh_jti 101 + ) 102 + .fetch_optional(&self.pool) 103 + .await 104 + .map_err(map_sqlx_error)?; 105 + 106 + Ok(row.map(|r| SessionForRefresh { 107 + id: r.id, 108 + did: Did::from(r.did), 109 + scope: r.scope, 110 + controller_did: r.controller_did.map(Did::from), 111 + key_bytes: r.key_bytes, 112 + encryption_version: r.encryption_version.unwrap_or(0), 113 + })) 114 + } 115 + 116 + async fn update_session_tokens( 117 + &self, 118 + session_id: i32, 119 + new_access_jti: &str, 120 + new_refresh_jti: &str, 121 + new_access_expires_at: DateTime<Utc>, 122 + new_refresh_expires_at: DateTime<Utc>, 123 + ) -> Result<(), DbError> { 124 + sqlx::query!( 125 + r#" 126 + UPDATE session_tokens 127 + SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, 128 + refresh_expires_at = $4, updated_at = NOW() 129 + WHERE id = $5 130 + "#, 131 + new_access_jti, 132 + new_refresh_jti, 133 + new_access_expires_at, 134 + new_refresh_expires_at, 135 + session_id 136 + ) 137 + .execute(&self.pool) 138 + .await 139 + .map_err(map_sqlx_error)?; 140 + 141 + Ok(()) 142 + } 143 + 144 + async fn delete_session_by_access_jti(&self, access_jti: &str) -> Result<u64, DbError> { 145 + let result = sqlx::query!( 146 + "DELETE FROM session_tokens WHERE access_jti = $1", 147 + access_jti 148 + ) 149 + .execute(&self.pool) 150 + .await 151 + .map_err(map_sqlx_error)?; 152 + 153 + Ok(result.rows_affected()) 154 + } 155 + 156 + async fn delete_session_by_id(&self, session_id: i32) -> Result<u64, DbError> { 157 + let result = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_id) 158 + .execute(&self.pool) 159 + .await 160 + .map_err(map_sqlx_error)?; 161 + 162 + Ok(result.rows_affected()) 163 + } 164 + 165 + async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError> { 166 + let result = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did.as_str()) 167 + .execute(&self.pool) 168 + .await 169 + .map_err(map_sqlx_error)?; 170 + 171 + Ok(result.rows_affected()) 172 + } 173 + 174 + async fn delete_sessions_by_did_except_jti( 175 + &self, 176 + did: &Did, 177 + except_jti: &str, 178 + ) -> Result<u64, DbError> { 179 + let result = sqlx::query!( 180 + "DELETE FROM session_tokens WHERE did = $1 AND access_jti != $2", 181 + did.as_str(), 182 + except_jti 183 + ) 184 + .execute(&self.pool) 185 + .await 186 + .map_err(map_sqlx_error)?; 187 + 188 + Ok(result.rows_affected()) 189 + } 190 + 191 + async fn list_sessions_by_did(&self, did: &Did) -> Result<Vec<SessionListItem>, DbError> { 192 + let rows = sqlx::query!( 193 + r#" 194 + SELECT id, access_jti, created_at, refresh_expires_at 195 + FROM session_tokens 196 + WHERE did = $1 AND refresh_expires_at > NOW() 197 + ORDER BY created_at DESC 198 + "#, 199 + did.as_str() 200 + ) 201 + .fetch_all(&self.pool) 202 + .await 203 + .map_err(map_sqlx_error)?; 204 + 205 + Ok(rows 206 + .into_iter() 207 + .map(|r| SessionListItem { 208 + id: r.id, 209 + access_jti: r.access_jti, 210 + created_at: r.created_at, 211 + refresh_expires_at: r.refresh_expires_at, 212 + }) 213 + .collect()) 214 + } 215 + 216 + async fn get_session_access_jti_by_id( 217 + &self, 218 + session_id: i32, 219 + did: &Did, 220 + ) -> Result<Option<String>, DbError> { 221 + let row = sqlx::query_scalar!( 222 + "SELECT access_jti FROM session_tokens WHERE id = $1 AND did = $2", 223 + session_id, 224 + did.as_str() 225 + ) 226 + .fetch_optional(&self.pool) 227 + .await 228 + .map_err(map_sqlx_error)?; 229 + 230 + Ok(row) 231 + } 232 + 233 + async fn delete_sessions_by_app_password( 234 + &self, 235 + did: &Did, 236 + app_password_name: &str, 237 + ) -> Result<u64, DbError> { 238 + let result = sqlx::query!( 239 + "DELETE FROM session_tokens WHERE did = $1 AND app_password_name = $2", 240 + did.as_str(), 241 + app_password_name 242 + ) 243 + .execute(&self.pool) 244 + .await 245 + .map_err(map_sqlx_error)?; 246 + 247 + Ok(result.rows_affected()) 248 + } 249 + 250 + async fn get_session_jtis_by_app_password( 251 + &self, 252 + did: &Did, 253 + app_password_name: &str, 254 + ) -> Result<Vec<String>, DbError> { 255 + let rows = sqlx::query_scalar!( 256 + "SELECT access_jti FROM session_tokens WHERE did = $1 AND app_password_name = $2", 257 + did.as_str(), 258 + app_password_name 259 + ) 260 + .fetch_all(&self.pool) 261 + .await 262 + .map_err(map_sqlx_error)?; 263 + 264 + Ok(rows) 265 + } 266 + 267 + async fn check_refresh_token_used(&self, refresh_jti: &str) -> Result<Option<i32>, DbError> { 268 + let row = sqlx::query_scalar!( 269 + "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1", 270 + refresh_jti 271 + ) 272 + .fetch_optional(&self.pool) 273 + .await 274 + .map_err(map_sqlx_error)?; 275 + 276 + Ok(row) 277 + } 278 + 279 + async fn mark_refresh_token_used( 280 + &self, 281 + refresh_jti: &str, 282 + session_id: i32, 283 + ) -> Result<bool, DbError> { 284 + let result = sqlx::query!( 285 + r#" 286 + INSERT INTO used_refresh_tokens (refresh_jti, session_id) 287 + VALUES ($1, $2) 288 + ON CONFLICT (refresh_jti) DO NOTHING 289 + "#, 290 + refresh_jti, 291 + session_id 292 + ) 293 + .execute(&self.pool) 294 + .await 295 + .map_err(map_sqlx_error)?; 296 + 297 + Ok(result.rows_affected() > 0) 298 + } 299 + 300 + async fn list_app_passwords(&self, user_id: Uuid) -> Result<Vec<AppPasswordRecord>, DbError> { 301 + let rows = sqlx::query!( 302 + r#" 303 + SELECT id, user_id, name, password_hash, created_at, privileged, scopes, created_by_controller_did 304 + FROM app_passwords 305 + WHERE user_id = $1 306 + ORDER BY created_at DESC 307 + "#, 308 + user_id 309 + ) 310 + .fetch_all(&self.pool) 311 + .await 312 + .map_err(map_sqlx_error)?; 313 + 314 + Ok(rows 315 + .into_iter() 316 + .map(|r| AppPasswordRecord { 317 + id: r.id, 318 + user_id: r.user_id, 319 + name: r.name, 320 + password_hash: r.password_hash, 321 + created_at: r.created_at, 322 + privileged: r.privileged, 323 + scopes: r.scopes, 324 + created_by_controller_did: r.created_by_controller_did.map(Did::from), 325 + }) 326 + .collect()) 327 + } 328 + 329 + async fn get_app_passwords_for_login( 330 + &self, 331 + user_id: Uuid, 332 + ) -> Result<Vec<AppPasswordRecord>, DbError> { 333 + let rows = sqlx::query!( 334 + r#" 335 + SELECT id, user_id, name, password_hash, created_at, privileged, scopes, created_by_controller_did 336 + FROM app_passwords 337 + WHERE user_id = $1 338 + ORDER BY created_at DESC 339 + LIMIT 20 340 + "#, 341 + user_id 342 + ) 343 + .fetch_all(&self.pool) 344 + .await 345 + .map_err(map_sqlx_error)?; 346 + 347 + Ok(rows 348 + .into_iter() 349 + .map(|r| AppPasswordRecord { 350 + id: r.id, 351 + user_id: r.user_id, 352 + name: r.name, 353 + password_hash: r.password_hash, 354 + created_at: r.created_at, 355 + privileged: r.privileged, 356 + scopes: r.scopes, 357 + created_by_controller_did: r.created_by_controller_did.map(Did::from), 358 + }) 359 + .collect()) 360 + } 361 + 362 + async fn get_app_password_by_name( 363 + &self, 364 + user_id: Uuid, 365 + name: &str, 366 + ) -> Result<Option<AppPasswordRecord>, DbError> { 367 + let row = sqlx::query!( 368 + r#" 369 + SELECT id, user_id, name, password_hash, created_at, privileged, scopes, created_by_controller_did 370 + FROM app_passwords 371 + WHERE user_id = $1 AND name = $2 372 + "#, 373 + user_id, 374 + name 375 + ) 376 + .fetch_optional(&self.pool) 377 + .await 378 + .map_err(map_sqlx_error)?; 379 + 380 + Ok(row.map(|r| AppPasswordRecord { 381 + id: r.id, 382 + user_id: r.user_id, 383 + name: r.name, 384 + password_hash: r.password_hash, 385 + created_at: r.created_at, 386 + privileged: r.privileged, 387 + scopes: r.scopes, 388 + created_by_controller_did: r.created_by_controller_did.map(Did::from), 389 + })) 390 + } 391 + 392 + async fn create_app_password(&self, data: &AppPasswordCreate) -> Result<Uuid, DbError> { 393 + let row = sqlx::query!( 394 + r#" 395 + INSERT INTO app_passwords (user_id, name, password_hash, privileged, scopes, created_by_controller_did) 396 + VALUES ($1, $2, $3, $4, $5, $6) 397 + RETURNING id 398 + "#, 399 + data.user_id, 400 + data.name, 401 + data.password_hash, 402 + data.privileged, 403 + data.scopes, 404 + data.created_by_controller_did.as_ref().map(|d| d.as_str()) 405 + ) 406 + .fetch_one(&self.pool) 407 + .await 408 + .map_err(map_sqlx_error)?; 409 + 410 + Ok(row.id) 411 + } 412 + 413 + async fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result<u64, DbError> { 414 + let result = sqlx::query!( 415 + "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", 416 + user_id, 417 + name 418 + ) 419 + .execute(&self.pool) 420 + .await 421 + .map_err(map_sqlx_error)?; 422 + 423 + Ok(result.rows_affected()) 424 + } 425 + 426 + async fn delete_app_passwords_by_controller( 427 + &self, 428 + did: &Did, 429 + controller_did: &Did, 430 + ) -> Result<u64, DbError> { 431 + let result = sqlx::query!( 432 + r#"DELETE FROM app_passwords 433 + WHERE user_id = (SELECT id FROM users WHERE did = $1) 434 + AND created_by_controller_did = $2"#, 435 + did.as_str(), 436 + controller_did.as_str() 437 + ) 438 + .execute(&self.pool) 439 + .await 440 + .map_err(map_sqlx_error)?; 441 + 442 + Ok(result.rows_affected()) 443 + } 444 + 445 + async fn get_last_reauth_at(&self, did: &Did) -> Result<Option<DateTime<Utc>>, DbError> { 446 + let row = sqlx::query_scalar!( 447 + r#"SELECT last_reauth_at FROM session_tokens 448 + WHERE did = $1 ORDER BY created_at DESC LIMIT 1"#, 449 + did.as_str() 450 + ) 451 + .fetch_optional(&self.pool) 452 + .await 453 + .map_err(map_sqlx_error)?; 454 + 455 + Ok(row.flatten()) 456 + } 457 + 458 + async fn update_last_reauth(&self, did: &Did) -> Result<DateTime<Utc>, DbError> { 459 + let now = Utc::now(); 460 + sqlx::query!( 461 + "UPDATE session_tokens SET last_reauth_at = $1, mfa_verified = TRUE WHERE did = $2", 462 + now, 463 + did.as_str() 464 + ) 465 + .execute(&self.pool) 466 + .await 467 + .map_err(map_sqlx_error)?; 468 + 469 + Ok(now) 470 + } 471 + 472 + async fn get_session_mfa_status(&self, did: &Did) -> Result<Option<SessionMfaStatus>, DbError> { 473 + let row = sqlx::query!( 474 + r#"SELECT legacy_login, mfa_verified, last_reauth_at FROM session_tokens 475 + WHERE did = $1 ORDER BY created_at DESC LIMIT 1"#, 476 + did.as_str() 477 + ) 478 + .fetch_optional(&self.pool) 479 + .await 480 + .map_err(map_sqlx_error)?; 481 + 482 + Ok(row.map(|r| SessionMfaStatus { 483 + legacy_login: r.legacy_login, 484 + mfa_verified: r.mfa_verified, 485 + last_reauth_at: r.last_reauth_at, 486 + })) 487 + } 488 + 489 + async fn update_mfa_verified(&self, did: &Did) -> Result<(), DbError> { 490 + sqlx::query!( 491 + "UPDATE session_tokens SET mfa_verified = TRUE, last_reauth_at = NOW() WHERE did = $1", 492 + did.as_str() 493 + ) 494 + .execute(&self.pool) 495 + .await 496 + .map_err(map_sqlx_error)?; 497 + 498 + Ok(()) 499 + } 500 + 501 + async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError> { 502 + let rows = sqlx::query_scalar!( 503 + r#"SELECT ap.password_hash FROM app_passwords ap 504 + JOIN users u ON ap.user_id = u.id 505 + WHERE u.did = $1"#, 506 + did.as_str() 507 + ) 508 + .fetch_all(&self.pool) 509 + .await 510 + .map_err(map_sqlx_error)?; 511 + 512 + Ok(rows) 513 + } 514 + 515 + async fn refresh_session_atomic( 516 + &self, 517 + data: &SessionRefreshData, 518 + ) -> Result<RefreshSessionResult, DbError> { 519 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 520 + 521 + if let Ok(Some(session_id)) = sqlx::query_scalar!( 522 + "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1 FOR UPDATE", 523 + data.old_refresh_jti 524 + ) 525 + .fetch_optional(&mut *tx) 526 + .await 527 + { 528 + let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_id) 529 + .execute(&mut *tx) 530 + .await; 531 + tx.commit().await.map_err(map_sqlx_error)?; 532 + return Ok(RefreshSessionResult::TokenAlreadyUsed); 533 + } 534 + 535 + let result = sqlx::query!( 536 + "INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2) ON CONFLICT (refresh_jti) DO NOTHING", 537 + data.old_refresh_jti, 538 + data.session_id 539 + ) 540 + .execute(&mut *tx) 541 + .await 542 + .map_err(map_sqlx_error)?; 543 + 544 + if result.rows_affected() == 0 { 545 + let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", data.session_id) 546 + .execute(&mut *tx) 547 + .await; 548 + tx.commit().await.map_err(map_sqlx_error)?; 549 + return Ok(RefreshSessionResult::ConcurrentRefresh); 550 + } 551 + 552 + sqlx::query!( 553 + "UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5", 554 + data.new_access_jti, 555 + data.new_refresh_jti, 556 + data.new_access_expires_at, 557 + data.new_refresh_expires_at, 558 + data.session_id 559 + ) 560 + .execute(&mut *tx) 561 + .await 562 + .map_err(map_sqlx_error)?; 563 + 564 + tx.commit().await.map_err(map_sqlx_error)?; 565 + Ok(RefreshSessionResult::Success) 566 + } 567 + }
+2778
crates/tranquil-db/src/postgres/user.rs
··· 1 + use async_trait::async_trait; 2 + use chrono::{DateTime, Utc}; 3 + use sqlx::PgPool; 4 + use tranquil_types::{Did, Handle}; 5 + use uuid::Uuid; 6 + 7 + use tranquil_db_traits::{ 8 + AccountSearchResult, CommsChannel, DbError, DidWebOverrides, NotificationPrefs, 9 + OAuthTokenWithUser, PasswordResetResult, StoredBackupCode, StoredPasskey, TotpRecord, 10 + User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, 11 + UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, UserForPasskeySetup, 12 + UserForRecovery, UserForVerification, UserIdAndHandle, UserIdAndPasswordHash, UserIdHandleEmail, 13 + UserInfoForAuth, UserKeyInfo, UserKeyWithId, UserLegacyLoginPref, UserLoginCheck, UserLoginFull, 14 + UserLoginInfo, UserPasswordInfo, UserRepository, UserResendVerification, UserResetCodeInfo, 15 + UserRow, UserSessionInfo, UserStatus, UserVerificationInfo, UserWithKey, 16 + }; 17 + 18 + pub struct PostgresUserRepository { 19 + pool: PgPool, 20 + } 21 + 22 + impl PostgresUserRepository { 23 + pub fn new(pool: PgPool) -> Self { 24 + Self { pool } 25 + } 26 + } 27 + 28 + pub(crate) fn map_sqlx_error(e: sqlx::Error) -> DbError { 29 + match e { 30 + sqlx::Error::RowNotFound => DbError::NotFound, 31 + sqlx::Error::Database(db_err) => { 32 + let msg = db_err.message().to_string(); 33 + if db_err.is_unique_violation() || db_err.is_foreign_key_violation() { 34 + DbError::Constraint(msg) 35 + } else { 36 + DbError::Query(msg) 37 + } 38 + } 39 + sqlx::Error::PoolTimedOut => DbError::Connection("Pool timed out".into()), 40 + _ => DbError::Other(e.to_string()), 41 + } 42 + } 43 + 44 + #[async_trait] 45 + impl UserRepository for PostgresUserRepository { 46 + async fn get_by_did(&self, did: &Did) -> Result<Option<UserRow>, DbError> { 47 + let row = sqlx::query!( 48 + r#"SELECT id, did, handle, email, created_at, deactivated_at, takedown_ref, is_admin 49 + FROM users WHERE did = $1"#, 50 + did.as_str() 51 + ) 52 + .fetch_optional(&self.pool) 53 + .await 54 + .map_err(map_sqlx_error)?; 55 + 56 + Ok(row.map(|r| UserRow { 57 + id: r.id, 58 + did: Did::from(r.did), 59 + handle: Handle::from(r.handle), 60 + email: r.email, 61 + created_at: r.created_at, 62 + deactivated_at: r.deactivated_at, 63 + takedown_ref: r.takedown_ref, 64 + is_admin: r.is_admin, 65 + })) 66 + } 67 + 68 + async fn get_by_handle(&self, handle: &Handle) -> Result<Option<UserRow>, DbError> { 69 + let row = sqlx::query!( 70 + r#"SELECT id, did, handle, email, created_at, deactivated_at, takedown_ref, is_admin 71 + FROM users WHERE handle = $1"#, 72 + handle.as_str() 73 + ) 74 + .fetch_optional(&self.pool) 75 + .await 76 + .map_err(map_sqlx_error)?; 77 + 78 + Ok(row.map(|r| UserRow { 79 + id: r.id, 80 + did: Did::from(r.did), 81 + handle: Handle::from(r.handle), 82 + email: r.email, 83 + created_at: r.created_at, 84 + deactivated_at: r.deactivated_at, 85 + takedown_ref: r.takedown_ref, 86 + is_admin: r.is_admin, 87 + })) 88 + } 89 + 90 + async fn get_with_key_by_did(&self, did: &Did) -> Result<Option<UserWithKey>, DbError> { 91 + let row = sqlx::query!( 92 + r#"SELECT u.id, u.did, u.handle, u.email, u.deactivated_at, u.takedown_ref, u.is_admin, 93 + k.key_bytes, k.encryption_version 94 + FROM users u 95 + JOIN user_keys k ON u.id = k.user_id 96 + WHERE u.did = $1"#, 97 + did.as_str() 98 + ) 99 + .fetch_optional(&self.pool) 100 + .await 101 + .map_err(map_sqlx_error)?; 102 + 103 + Ok(row.map(|r| UserWithKey { 104 + id: r.id, 105 + did: Did::from(r.did), 106 + handle: Handle::from(r.handle), 107 + email: r.email, 108 + deactivated_at: r.deactivated_at, 109 + takedown_ref: r.takedown_ref, 110 + is_admin: r.is_admin, 111 + key_bytes: r.key_bytes, 112 + encryption_version: r.encryption_version, 113 + })) 114 + } 115 + 116 + async fn get_status_by_did(&self, did: &Did) -> Result<Option<UserStatus>, DbError> { 117 + let row = sqlx::query!( 118 + "SELECT deactivated_at, takedown_ref, is_admin FROM users WHERE did = $1", 119 + did.as_str() 120 + ) 121 + .fetch_optional(&self.pool) 122 + .await 123 + .map_err(map_sqlx_error)?; 124 + 125 + Ok(row.map(|r| UserStatus { 126 + deactivated_at: r.deactivated_at, 127 + takedown_ref: r.takedown_ref, 128 + is_admin: r.is_admin, 129 + })) 130 + } 131 + 132 + async fn count_users(&self) -> Result<i64, DbError> { 133 + let row = sqlx::query_scalar!("SELECT COUNT(*) FROM users") 134 + .fetch_one(&self.pool) 135 + .await 136 + .map_err(map_sqlx_error)?; 137 + Ok(row.unwrap_or(0)) 138 + } 139 + 140 + async fn get_session_access_expiry( 141 + &self, 142 + did: &Did, 143 + access_jti: &str, 144 + ) -> Result<Option<DateTime<Utc>>, DbError> { 145 + let row = sqlx::query!( 146 + "SELECT access_expires_at FROM session_tokens WHERE did = $1 AND access_jti = $2", 147 + did.as_str(), 148 + access_jti 149 + ) 150 + .fetch_optional(&self.pool) 151 + .await 152 + .map_err(map_sqlx_error)?; 153 + 154 + Ok(row.map(|r| r.access_expires_at)) 155 + } 156 + 157 + async fn get_oauth_token_with_user( 158 + &self, 159 + token_id: &str, 160 + ) -> Result<Option<OAuthTokenWithUser>, DbError> { 161 + let row = sqlx::query!( 162 + r#"SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref, u.is_admin, 163 + k.key_bytes as "key_bytes?", k.encryption_version as "encryption_version?" 164 + FROM oauth_token t 165 + JOIN users u ON t.did = u.did 166 + LEFT JOIN user_keys k ON u.id = k.user_id 167 + WHERE t.token_id = $1"#, 168 + token_id 169 + ) 170 + .fetch_optional(&self.pool) 171 + .await 172 + .map_err(map_sqlx_error)?; 173 + 174 + Ok(row.map(|r| OAuthTokenWithUser { 175 + did: Did::from(r.did), 176 + expires_at: r.expires_at, 177 + deactivated_at: r.deactivated_at, 178 + takedown_ref: r.takedown_ref, 179 + is_admin: r.is_admin, 180 + key_bytes: r.key_bytes, 181 + encryption_version: r.encryption_version, 182 + })) 183 + } 184 + 185 + async fn get_user_info_by_did(&self, did: &Did) -> Result<Option<UserInfoForAuth>, DbError> { 186 + let row = sqlx::query!( 187 + r#"SELECT u.deactivated_at, u.takedown_ref, u.is_admin, 188 + k.key_bytes as "key_bytes?", k.encryption_version as "encryption_version?" 189 + FROM users u 190 + LEFT JOIN user_keys k ON u.id = k.user_id 191 + WHERE u.did = $1"#, 192 + did.as_str() 193 + ) 194 + .fetch_optional(&self.pool) 195 + .await 196 + .map_err(map_sqlx_error)?; 197 + 198 + Ok(row.map(|r| UserInfoForAuth { 199 + deactivated_at: r.deactivated_at, 200 + takedown_ref: r.takedown_ref, 201 + is_admin: r.is_admin, 202 + key_bytes: r.key_bytes, 203 + encryption_version: r.encryption_version, 204 + })) 205 + } 206 + 207 + async fn get_any_admin_user_id(&self) -> Result<Option<Uuid>, DbError> { 208 + let row = sqlx::query_scalar!("SELECT id FROM users WHERE is_admin = true LIMIT 1") 209 + .fetch_optional(&self.pool) 210 + .await 211 + .map_err(map_sqlx_error)?; 212 + Ok(row) 213 + } 214 + 215 + async fn set_invites_disabled(&self, did: &Did, disabled: bool) -> Result<bool, DbError> { 216 + let result = sqlx::query!( 217 + "UPDATE users SET invites_disabled = $2 WHERE did = $1", 218 + did.as_str(), 219 + disabled 220 + ) 221 + .execute(&self.pool) 222 + .await 223 + .map_err(map_sqlx_error)?; 224 + Ok(result.rows_affected() > 0) 225 + } 226 + 227 + async fn search_accounts( 228 + &self, 229 + cursor_did: Option<&Did>, 230 + email_filter: Option<&str>, 231 + handle_filter: Option<&str>, 232 + limit: i64, 233 + ) -> Result<Vec<AccountSearchResult>, DbError> { 234 + let cursor_str = cursor_did.map(|d| d.as_str()); 235 + let rows = sqlx::query!( 236 + r#"SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled 237 + FROM users 238 + WHERE ($1::text IS NULL OR did > $1) 239 + AND ($2::text IS NULL OR email ILIKE $2) 240 + AND ($3::text IS NULL OR handle ILIKE $3) 241 + ORDER BY did ASC 242 + LIMIT $4"#, 243 + cursor_str, 244 + email_filter, 245 + handle_filter, 246 + limit 247 + ) 248 + .fetch_all(&self.pool) 249 + .await 250 + .map_err(map_sqlx_error)?; 251 + Ok(rows 252 + .into_iter() 253 + .map(|r| AccountSearchResult { 254 + did: Did::from(r.did), 255 + handle: Handle::from(r.handle), 256 + email: r.email, 257 + created_at: r.created_at, 258 + email_verified: r.email_verified, 259 + deactivated_at: r.deactivated_at, 260 + invites_disabled: r.invites_disabled, 261 + }) 262 + .collect()) 263 + } 264 + 265 + async fn get_auth_info_by_did(&self, did: &Did) -> Result<Option<UserAuthInfo>, DbError> { 266 + let row = sqlx::query!( 267 + r#"SELECT id, did, password_hash, deactivated_at, takedown_ref, 268 + email_verified, discord_verified, telegram_verified, signal_verified 269 + FROM users 270 + WHERE did = $1"#, 271 + did.as_str() 272 + ) 273 + .fetch_optional(&self.pool) 274 + .await 275 + .map_err(map_sqlx_error)?; 276 + Ok(row.map(|r| UserAuthInfo { 277 + id: r.id, 278 + did: Did::from(r.did), 279 + password_hash: r.password_hash, 280 + deactivated_at: r.deactivated_at, 281 + takedown_ref: r.takedown_ref, 282 + email_verified: r.email_verified, 283 + discord_verified: r.discord_verified, 284 + telegram_verified: r.telegram_verified, 285 + signal_verified: r.signal_verified, 286 + })) 287 + } 288 + 289 + async fn get_by_email(&self, email: &str) -> Result<Option<UserForVerification>, DbError> { 290 + let row = sqlx::query!( 291 + r#"SELECT id, did, email, email_verified, handle 292 + FROM users 293 + WHERE LOWER(email) = $1"#, 294 + email 295 + ) 296 + .fetch_optional(&self.pool) 297 + .await 298 + .map_err(map_sqlx_error)?; 299 + Ok(row.map(|r| UserForVerification { 300 + id: r.id, 301 + did: Did::from(r.did), 302 + email: r.email, 303 + email_verified: r.email_verified, 304 + handle: Handle::from(r.handle), 305 + })) 306 + } 307 + 308 + async fn get_comms_prefs(&self, user_id: Uuid) -> Result<Option<UserCommsPrefs>, DbError> { 309 + let row = sqlx::query!( 310 + r#"SELECT email, handle, preferred_comms_channel::text as "preferred_channel!", preferred_locale 311 + FROM users WHERE id = $1"#, 312 + user_id 313 + ) 314 + .fetch_optional(&self.pool) 315 + .await 316 + .map_err(map_sqlx_error)?; 317 + Ok(row.map(|r| UserCommsPrefs { 318 + email: r.email, 319 + handle: Handle::from(r.handle), 320 + preferred_channel: r.preferred_channel, 321 + preferred_locale: r.preferred_locale, 322 + })) 323 + } 324 + 325 + async fn get_id_by_did(&self, did: &Did) -> Result<Option<Uuid>, DbError> { 326 + let id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str()) 327 + .fetch_optional(&self.pool) 328 + .await 329 + .map_err(map_sqlx_error)?; 330 + Ok(id) 331 + } 332 + 333 + async fn get_user_key_by_id(&self, user_id: Uuid) -> Result<Option<UserKeyInfo>, DbError> { 334 + let row = sqlx::query!( 335 + "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 336 + user_id 337 + ) 338 + .fetch_optional(&self.pool) 339 + .await 340 + .map_err(map_sqlx_error)?; 341 + Ok(row.map(|r| UserKeyInfo { 342 + key_bytes: r.key_bytes, 343 + encryption_version: r.encryption_version, 344 + })) 345 + } 346 + 347 + async fn get_id_and_handle_by_did(&self, did: &Did) -> Result<Option<UserIdAndHandle>, DbError> { 348 + let row = sqlx::query!( 349 + "SELECT id, handle FROM users WHERE did = $1", 350 + did.as_str() 351 + ) 352 + .fetch_optional(&self.pool) 353 + .await 354 + .map_err(map_sqlx_error)?; 355 + Ok(row.map(|r| UserIdAndHandle { 356 + id: r.id, 357 + handle: Handle::from(r.handle), 358 + })) 359 + } 360 + 361 + async fn get_did_web_info_by_handle( 362 + &self, 363 + handle: &Handle, 364 + ) -> Result<Option<UserDidWebInfo>, DbError> { 365 + let row = sqlx::query!( 366 + "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1", 367 + handle.as_str() 368 + ) 369 + .fetch_optional(&self.pool) 370 + .await 371 + .map_err(map_sqlx_error)?; 372 + Ok(row.map(|r| UserDidWebInfo { 373 + id: r.id, 374 + did: Did::from(r.did), 375 + migrated_to_pds: r.migrated_to_pds, 376 + })) 377 + } 378 + 379 + async fn get_did_web_overrides(&self, user_id: Uuid) -> Result<Option<DidWebOverrides>, DbError> { 380 + let row = sqlx::query!( 381 + "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1", 382 + user_id 383 + ) 384 + .fetch_optional(&self.pool) 385 + .await 386 + .map_err(map_sqlx_error)?; 387 + Ok(row.map(|r| DidWebOverrides { 388 + verification_methods: r.verification_methods, 389 + also_known_as: r.also_known_as, 390 + })) 391 + } 392 + 393 + async fn get_handle_by_did(&self, did: &Did) -> Result<Option<Handle>, DbError> { 394 + let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did.as_str()) 395 + .fetch_optional(&self.pool) 396 + .await 397 + .map_err(map_sqlx_error)?; 398 + Ok(handle.map(Handle::from)) 399 + } 400 + 401 + async fn check_handle_exists(&self, handle: &Handle, exclude_user_id: Uuid) -> Result<bool, DbError> { 402 + let exists = sqlx::query_scalar!( 403 + "SELECT EXISTS(SELECT 1 FROM users WHERE handle = $1 AND id != $2) as \"exists!\"", 404 + handle.as_str(), 405 + exclude_user_id 406 + ) 407 + .fetch_one(&self.pool) 408 + .await 409 + .map_err(map_sqlx_error)?; 410 + Ok(exists) 411 + } 412 + 413 + async fn update_handle(&self, user_id: Uuid, handle: &Handle) -> Result<(), DbError> { 414 + sqlx::query!( 415 + "UPDATE users SET handle = $1 WHERE id = $2", 416 + handle.as_str(), 417 + user_id 418 + ) 419 + .execute(&self.pool) 420 + .await 421 + .map_err(map_sqlx_error)?; 422 + Ok(()) 423 + } 424 + 425 + async fn get_user_with_key_by_did( 426 + &self, 427 + did: &Did, 428 + ) -> Result<Option<UserKeyWithId>, DbError> { 429 + let row = sqlx::query!( 430 + r#"SELECT u.id, uk.key_bytes, uk.encryption_version 431 + FROM users u 432 + JOIN user_keys uk ON u.id = uk.user_id 433 + WHERE u.did = $1"#, 434 + did.as_str() 435 + ) 436 + .fetch_optional(&self.pool) 437 + .await 438 + .map_err(map_sqlx_error)?; 439 + Ok(row.map(|r| UserKeyWithId { 440 + id: r.id, 441 + key_bytes: r.key_bytes, 442 + encryption_version: r.encryption_version, 443 + })) 444 + } 445 + 446 + async fn is_account_migrated(&self, did: &Did) -> Result<bool, DbError> { 447 + let row = sqlx::query!( 448 + r#"SELECT (migrated_to_pds IS NOT NULL AND deactivated_at IS NOT NULL) as "migrated!: bool" FROM users WHERE did = $1"#, 449 + did.as_str() 450 + ) 451 + .fetch_optional(&self.pool) 452 + .await 453 + .map_err(map_sqlx_error)?; 454 + Ok(row.map(|r| r.migrated).unwrap_or(false)) 455 + } 456 + 457 + async fn has_verified_comms_channel(&self, did: &Did) -> Result<bool, DbError> { 458 + let row = sqlx::query!( 459 + r#"SELECT 460 + email_verified, 461 + discord_verified, 462 + telegram_verified, 463 + signal_verified 464 + FROM users 465 + WHERE did = $1"#, 466 + did.as_str() 467 + ) 468 + .fetch_optional(&self.pool) 469 + .await 470 + .map_err(map_sqlx_error)?; 471 + Ok(row 472 + .map(|r| r.email_verified || r.discord_verified || r.telegram_verified || r.signal_verified) 473 + .unwrap_or(false)) 474 + } 475 + 476 + async fn get_id_by_handle(&self, handle: &Handle) -> Result<Option<Uuid>, DbError> { 477 + let id = sqlx::query_scalar!("SELECT id FROM users WHERE handle = $1", handle.as_str()) 478 + .fetch_optional(&self.pool) 479 + .await 480 + .map_err(map_sqlx_error)?; 481 + Ok(id) 482 + } 483 + 484 + async fn get_email_info_by_did(&self, did: &Did) -> Result<Option<UserEmailInfo>, DbError> { 485 + let row = sqlx::query!( 486 + "SELECT id, handle, email, email_verified FROM users WHERE did = $1", 487 + did.as_str() 488 + ) 489 + .fetch_optional(&self.pool) 490 + .await 491 + .map_err(map_sqlx_error)?; 492 + Ok(row.map(|r| UserEmailInfo { 493 + id: r.id, 494 + handle: Handle::from(r.handle), 495 + email: r.email, 496 + email_verified: r.email_verified, 497 + })) 498 + } 499 + 500 + async fn check_email_exists(&self, email: &str, exclude_user_id: Uuid) -> Result<bool, DbError> { 501 + let row = sqlx::query!( 502 + "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 503 + email.to_lowercase(), 504 + exclude_user_id 505 + ) 506 + .fetch_optional(&self.pool) 507 + .await 508 + .map_err(map_sqlx_error)?; 509 + Ok(row.is_some()) 510 + } 511 + 512 + async fn update_email(&self, user_id: Uuid, email: &str) -> Result<(), DbError> { 513 + sqlx::query!( 514 + "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2", 515 + email, 516 + user_id 517 + ) 518 + .execute(&self.pool) 519 + .await 520 + .map_err(map_sqlx_error)?; 521 + Ok(()) 522 + } 523 + 524 + async fn set_email_verified(&self, user_id: Uuid, verified: bool) -> Result<(), DbError> { 525 + sqlx::query!( 526 + "UPDATE users SET email_verified = $1, updated_at = NOW() WHERE id = $2", 527 + verified, 528 + user_id 529 + ) 530 + .execute(&self.pool) 531 + .await 532 + .map_err(map_sqlx_error)?; 533 + Ok(()) 534 + } 535 + 536 + async fn check_email_verified_by_identifier( 537 + &self, 538 + identifier: &str, 539 + ) -> Result<Option<bool>, DbError> { 540 + let row = sqlx::query_scalar!( 541 + "SELECT email_verified FROM users WHERE email = $1 OR handle = $1", 542 + identifier 543 + ) 544 + .fetch_optional(&self.pool) 545 + .await 546 + .map_err(map_sqlx_error)?; 547 + Ok(row) 548 + } 549 + 550 + async fn admin_update_email(&self, did: &Did, email: &str) -> Result<u64, DbError> { 551 + let result = sqlx::query!( 552 + "UPDATE users SET email = $1 WHERE did = $2", 553 + email, 554 + did.as_str() 555 + ) 556 + .execute(&self.pool) 557 + .await 558 + .map_err(map_sqlx_error)?; 559 + Ok(result.rows_affected()) 560 + } 561 + 562 + async fn admin_update_handle(&self, did: &Did, handle: &Handle) -> Result<u64, DbError> { 563 + let result = sqlx::query!( 564 + "UPDATE users SET handle = $1 WHERE did = $2", 565 + handle.as_str(), 566 + did.as_str() 567 + ) 568 + .execute(&self.pool) 569 + .await 570 + .map_err(map_sqlx_error)?; 571 + Ok(result.rows_affected()) 572 + } 573 + 574 + async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError> { 575 + let result = sqlx::query!( 576 + "UPDATE users SET password_hash = $1 WHERE did = $2", 577 + password_hash, 578 + did.as_str() 579 + ) 580 + .execute(&self.pool) 581 + .await 582 + .map_err(map_sqlx_error)?; 583 + Ok(result.rows_affected()) 584 + } 585 + 586 + async fn get_notification_prefs(&self, did: &Did) -> Result<Option<NotificationPrefs>, DbError> { 587 + let row = sqlx::query!( 588 + r#"SELECT 589 + email, 590 + preferred_comms_channel::text as "preferred_channel!", 591 + discord_id, 592 + discord_verified, 593 + telegram_username, 594 + telegram_verified, 595 + signal_number, 596 + signal_verified 597 + FROM users WHERE did = $1"#, 598 + did.as_str() 599 + ) 600 + .fetch_optional(&self.pool) 601 + .await 602 + .map_err(map_sqlx_error)?; 603 + Ok(row.map(|r| NotificationPrefs { 604 + email: r.email.unwrap_or_default(), 605 + preferred_channel: r.preferred_channel, 606 + discord_id: r.discord_id, 607 + discord_verified: r.discord_verified, 608 + telegram_username: r.telegram_username, 609 + telegram_verified: r.telegram_verified, 610 + signal_number: r.signal_number, 611 + signal_verified: r.signal_verified, 612 + })) 613 + } 614 + 615 + async fn get_id_handle_email_by_did( 616 + &self, 617 + did: &Did, 618 + ) -> Result<Option<UserIdHandleEmail>, DbError> { 619 + let row = sqlx::query!( 620 + "SELECT id, handle, email FROM users WHERE did = $1", 621 + did.as_str() 622 + ) 623 + .fetch_optional(&self.pool) 624 + .await 625 + .map_err(map_sqlx_error)?; 626 + Ok(row.map(|r| UserIdHandleEmail { 627 + id: r.id, 628 + handle: Handle::from(r.handle), 629 + email: r.email, 630 + })) 631 + } 632 + 633 + async fn update_preferred_comms_channel(&self, did: &Did, channel: &str) -> Result<(), DbError> { 634 + sqlx::query( 635 + "UPDATE users SET preferred_comms_channel = $1::comms_channel, updated_at = NOW() WHERE did = $2", 636 + ) 637 + .bind(channel) 638 + .bind(did.as_str()) 639 + .execute(&self.pool) 640 + .await 641 + .map_err(map_sqlx_error)?; 642 + Ok(()) 643 + } 644 + 645 + async fn clear_discord(&self, user_id: Uuid) -> Result<(), DbError> { 646 + sqlx::query!( 647 + "UPDATE users SET discord_id = NULL, discord_verified = FALSE, updated_at = NOW() WHERE id = $1", 648 + user_id 649 + ) 650 + .execute(&self.pool) 651 + .await 652 + .map_err(map_sqlx_error)?; 653 + Ok(()) 654 + } 655 + 656 + async fn clear_telegram(&self, user_id: Uuid) -> Result<(), DbError> { 657 + sqlx::query!( 658 + "UPDATE users SET telegram_username = NULL, telegram_verified = FALSE, updated_at = NOW() WHERE id = $1", 659 + user_id 660 + ) 661 + .execute(&self.pool) 662 + .await 663 + .map_err(map_sqlx_error)?; 664 + Ok(()) 665 + } 666 + 667 + async fn clear_signal(&self, user_id: Uuid) -> Result<(), DbError> { 668 + sqlx::query!( 669 + "UPDATE users SET signal_number = NULL, signal_verified = FALSE, updated_at = NOW() WHERE id = $1", 670 + user_id 671 + ) 672 + .execute(&self.pool) 673 + .await 674 + .map_err(map_sqlx_error)?; 675 + Ok(()) 676 + } 677 + 678 + async fn get_verification_info( 679 + &self, 680 + did: &Did, 681 + ) -> Result<Option<UserVerificationInfo>, DbError> { 682 + let row = sqlx::query!( 683 + r#"SELECT id, handle, email, email_verified, discord_verified, telegram_verified, signal_verified 684 + FROM users WHERE did = $1"#, 685 + did.as_str() 686 + ) 687 + .fetch_optional(&self.pool) 688 + .await 689 + .map_err(map_sqlx_error)?; 690 + Ok(row.map(|r| UserVerificationInfo { 691 + id: r.id, 692 + handle: Handle::from(r.handle), 693 + email: r.email, 694 + email_verified: r.email_verified, 695 + discord_verified: r.discord_verified, 696 + telegram_verified: r.telegram_verified, 697 + signal_verified: r.signal_verified, 698 + })) 699 + } 700 + 701 + async fn verify_email_channel(&self, user_id: Uuid, email: &str) -> Result<bool, DbError> { 702 + let result = sqlx::query!( 703 + "UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2", 704 + email, 705 + user_id 706 + ) 707 + .execute(&self.pool) 708 + .await; 709 + match result { 710 + Ok(_) => Ok(true), 711 + Err(e) => { 712 + if e.as_database_error() 713 + .map(|db| db.is_unique_violation()) 714 + .unwrap_or(false) 715 + { 716 + Ok(false) 717 + } else { 718 + Err(map_sqlx_error(e)) 719 + } 720 + } 721 + } 722 + } 723 + 724 + async fn verify_discord_channel(&self, user_id: Uuid, discord_id: &str) -> Result<(), DbError> { 725 + sqlx::query!( 726 + "UPDATE users SET discord_id = $1, discord_verified = TRUE, updated_at = NOW() WHERE id = $2", 727 + discord_id, 728 + user_id 729 + ) 730 + .execute(&self.pool) 731 + .await 732 + .map_err(map_sqlx_error)?; 733 + Ok(()) 734 + } 735 + 736 + async fn verify_telegram_channel( 737 + &self, 738 + user_id: Uuid, 739 + telegram_username: &str, 740 + ) -> Result<(), DbError> { 741 + sqlx::query!( 742 + "UPDATE users SET telegram_username = $1, telegram_verified = TRUE, updated_at = NOW() WHERE id = $2", 743 + telegram_username, 744 + user_id 745 + ) 746 + .execute(&self.pool) 747 + .await 748 + .map_err(map_sqlx_error)?; 749 + Ok(()) 750 + } 751 + 752 + async fn verify_signal_channel( 753 + &self, 754 + user_id: Uuid, 755 + signal_number: &str, 756 + ) -> Result<(), DbError> { 757 + sqlx::query!( 758 + "UPDATE users SET signal_number = $1, signal_verified = TRUE, updated_at = NOW() WHERE id = $2", 759 + signal_number, 760 + user_id 761 + ) 762 + .execute(&self.pool) 763 + .await 764 + .map_err(map_sqlx_error)?; 765 + Ok(()) 766 + } 767 + 768 + async fn set_email_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { 769 + sqlx::query!( 770 + "UPDATE users SET email_verified = TRUE WHERE id = $1", 771 + user_id 772 + ) 773 + .execute(&self.pool) 774 + .await 775 + .map_err(map_sqlx_error)?; 776 + Ok(()) 777 + } 778 + 779 + async fn set_discord_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { 780 + sqlx::query!( 781 + "UPDATE users SET discord_verified = TRUE WHERE id = $1", 782 + user_id 783 + ) 784 + .execute(&self.pool) 785 + .await 786 + .map_err(map_sqlx_error)?; 787 + Ok(()) 788 + } 789 + 790 + async fn set_telegram_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { 791 + sqlx::query!( 792 + "UPDATE users SET telegram_verified = TRUE WHERE id = $1", 793 + user_id 794 + ) 795 + .execute(&self.pool) 796 + .await 797 + .map_err(map_sqlx_error)?; 798 + Ok(()) 799 + } 800 + 801 + async fn set_signal_verified_flag(&self, user_id: Uuid) -> Result<(), DbError> { 802 + sqlx::query!( 803 + "UPDATE users SET signal_verified = TRUE WHERE id = $1", 804 + user_id 805 + ) 806 + .execute(&self.pool) 807 + .await 808 + .map_err(map_sqlx_error)?; 809 + Ok(()) 810 + } 811 + 812 + async fn has_totp_enabled(&self, did: &Did) -> Result<bool, DbError> { 813 + let row = sqlx::query_scalar!( 814 + "SELECT verified FROM user_totp WHERE did = $1", 815 + did.as_str() 816 + ) 817 + .fetch_optional(&self.pool) 818 + .await 819 + .map_err(map_sqlx_error)?; 820 + 821 + Ok(matches!(row, Some(true))) 822 + } 823 + 824 + async fn has_passkeys(&self, did: &Did) -> Result<bool, DbError> { 825 + let count = sqlx::query_scalar!( 826 + "SELECT COUNT(*) as count FROM passkeys WHERE did = $1", 827 + did.as_str() 828 + ) 829 + .fetch_one(&self.pool) 830 + .await 831 + .map_err(map_sqlx_error)?; 832 + 833 + Ok(count.unwrap_or(0) > 0) 834 + } 835 + 836 + async fn get_password_hash_by_did(&self, did: &Did) -> Result<Option<String>, DbError> { 837 + let row = sqlx::query_scalar!( 838 + "SELECT password_hash FROM users WHERE did = $1", 839 + did.as_str() 840 + ) 841 + .fetch_optional(&self.pool) 842 + .await 843 + .map_err(map_sqlx_error)?; 844 + 845 + Ok(row.flatten()) 846 + } 847 + 848 + async fn get_passkeys_for_user(&self, did: &Did) -> Result<Vec<StoredPasskey>, DbError> { 849 + let rows = sqlx::query!( 850 + r#"SELECT id, did, credential_id, public_key, sign_count, created_at, last_used, 851 + friendly_name, aaguid, transports 852 + FROM passkeys WHERE did = $1 ORDER BY created_at DESC"#, 853 + did.as_str() 854 + ) 855 + .fetch_all(&self.pool) 856 + .await 857 + .map_err(map_sqlx_error)?; 858 + 859 + Ok(rows 860 + .into_iter() 861 + .map(|r| StoredPasskey { 862 + id: r.id, 863 + did: Did::from(r.did), 864 + credential_id: r.credential_id, 865 + public_key: r.public_key, 866 + sign_count: r.sign_count, 867 + created_at: r.created_at, 868 + last_used: r.last_used, 869 + friendly_name: r.friendly_name, 870 + aaguid: r.aaguid, 871 + transports: r.transports, 872 + }) 873 + .collect()) 874 + } 875 + 876 + async fn get_passkey_by_credential_id( 877 + &self, 878 + credential_id: &[u8], 879 + ) -> Result<Option<StoredPasskey>, DbError> { 880 + let row = sqlx::query!( 881 + r#"SELECT id, did, credential_id, public_key, sign_count, created_at, last_used, 882 + friendly_name, aaguid, transports 883 + FROM passkeys WHERE credential_id = $1"#, 884 + credential_id 885 + ) 886 + .fetch_optional(&self.pool) 887 + .await 888 + .map_err(map_sqlx_error)?; 889 + 890 + Ok(row.map(|r| StoredPasskey { 891 + id: r.id, 892 + did: Did::from(r.did), 893 + credential_id: r.credential_id, 894 + public_key: r.public_key, 895 + sign_count: r.sign_count, 896 + created_at: r.created_at, 897 + last_used: r.last_used, 898 + friendly_name: r.friendly_name, 899 + aaguid: r.aaguid, 900 + transports: r.transports, 901 + })) 902 + } 903 + 904 + async fn save_passkey( 905 + &self, 906 + did: &Did, 907 + credential_id: &[u8], 908 + public_key: &[u8], 909 + friendly_name: Option<&str>, 910 + ) -> Result<Uuid, DbError> { 911 + let id = Uuid::new_v4(); 912 + let aaguid: Option<Vec<u8>> = None; 913 + sqlx::query!( 914 + r#"INSERT INTO passkeys (id, did, credential_id, public_key, sign_count, friendly_name, aaguid) 915 + VALUES ($1, $2, $3, $4, 0, $5, $6)"#, 916 + id, 917 + did.as_str(), 918 + credential_id, 919 + public_key, 920 + friendly_name, 921 + aaguid, 922 + ) 923 + .execute(&self.pool) 924 + .await 925 + .map_err(map_sqlx_error)?; 926 + 927 + Ok(id) 928 + } 929 + 930 + async fn update_passkey_counter( 931 + &self, 932 + credential_id: &[u8], 933 + new_counter: i32, 934 + ) -> Result<bool, DbError> { 935 + let stored = self.get_passkey_by_credential_id(credential_id).await?; 936 + let Some(stored) = stored else { 937 + return Err(DbError::NotFound); 938 + }; 939 + 940 + if new_counter > 0 && new_counter <= stored.sign_count { 941 + return Ok(false); 942 + } 943 + 944 + sqlx::query!( 945 + "UPDATE passkeys SET sign_count = $1, last_used = NOW() WHERE credential_id = $2", 946 + new_counter, 947 + credential_id, 948 + ) 949 + .execute(&self.pool) 950 + .await 951 + .map_err(map_sqlx_error)?; 952 + 953 + Ok(true) 954 + } 955 + 956 + async fn delete_passkey(&self, id: Uuid, did: &Did) -> Result<bool, DbError> { 957 + let result = sqlx::query!( 958 + "DELETE FROM passkeys WHERE id = $1 AND did = $2", 959 + id, 960 + did.as_str() 961 + ) 962 + .execute(&self.pool) 963 + .await 964 + .map_err(map_sqlx_error)?; 965 + 966 + Ok(result.rows_affected() > 0) 967 + } 968 + 969 + async fn update_passkey_name(&self, id: Uuid, did: &Did, name: &str) -> Result<bool, DbError> { 970 + let result = sqlx::query!( 971 + "UPDATE passkeys SET friendly_name = $1 WHERE id = $2 AND did = $3", 972 + name, 973 + id, 974 + did.as_str() 975 + ) 976 + .execute(&self.pool) 977 + .await 978 + .map_err(map_sqlx_error)?; 979 + 980 + Ok(result.rows_affected() > 0) 981 + } 982 + 983 + async fn save_webauthn_challenge( 984 + &self, 985 + did: &Did, 986 + challenge_type: &str, 987 + state_json: &str, 988 + ) -> Result<Uuid, DbError> { 989 + let id = Uuid::new_v4(); 990 + let challenge = id.as_bytes().to_vec(); 991 + let expires_at = chrono::Utc::now() + chrono::Duration::minutes(5); 992 + sqlx::query!( 993 + r#"INSERT INTO webauthn_challenges (id, did, challenge, challenge_type, state_json, expires_at) 994 + VALUES ($1, $2, $3, $4, $5, $6)"#, 995 + id, 996 + did.as_str(), 997 + challenge, 998 + challenge_type, 999 + state_json, 1000 + expires_at, 1001 + ) 1002 + .execute(&self.pool) 1003 + .await 1004 + .map_err(map_sqlx_error)?; 1005 + 1006 + Ok(id) 1007 + } 1008 + 1009 + async fn load_webauthn_challenge( 1010 + &self, 1011 + did: &Did, 1012 + challenge_type: &str, 1013 + ) -> Result<Option<String>, DbError> { 1014 + let row = sqlx::query_scalar!( 1015 + r#"SELECT state_json FROM webauthn_challenges 1016 + WHERE did = $1 AND challenge_type = $2 AND expires_at > NOW() 1017 + ORDER BY created_at DESC LIMIT 1"#, 1018 + did.as_str(), 1019 + challenge_type 1020 + ) 1021 + .fetch_optional(&self.pool) 1022 + .await 1023 + .map_err(map_sqlx_error)?; 1024 + 1025 + Ok(row) 1026 + } 1027 + 1028 + async fn delete_webauthn_challenge( 1029 + &self, 1030 + did: &Did, 1031 + challenge_type: &str, 1032 + ) -> Result<(), DbError> { 1033 + sqlx::query!( 1034 + "DELETE FROM webauthn_challenges WHERE did = $1 AND challenge_type = $2", 1035 + did.as_str(), 1036 + challenge_type 1037 + ) 1038 + .execute(&self.pool) 1039 + .await 1040 + .map_err(map_sqlx_error)?; 1041 + 1042 + Ok(()) 1043 + } 1044 + 1045 + async fn get_totp_record(&self, did: &Did) -> Result<Option<TotpRecord>, DbError> { 1046 + let row = sqlx::query!( 1047 + "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 1048 + did.as_str() 1049 + ) 1050 + .fetch_optional(&self.pool) 1051 + .await 1052 + .map_err(map_sqlx_error)?; 1053 + 1054 + Ok(row.map(|r| TotpRecord { 1055 + secret_encrypted: r.secret_encrypted, 1056 + encryption_version: r.encryption_version, 1057 + verified: r.verified, 1058 + })) 1059 + } 1060 + 1061 + async fn upsert_totp_secret( 1062 + &self, 1063 + did: &Did, 1064 + secret_encrypted: &[u8], 1065 + encryption_version: i32, 1066 + ) -> Result<(), DbError> { 1067 + sqlx::query!( 1068 + r#"INSERT INTO user_totp (did, secret_encrypted, encryption_version, verified, created_at) 1069 + VALUES ($1, $2, $3, false, NOW()) 1070 + ON CONFLICT (did) DO UPDATE SET 1071 + secret_encrypted = $2, 1072 + encryption_version = $3, 1073 + verified = false, 1074 + created_at = NOW(), 1075 + last_used = NULL"#, 1076 + did.as_str(), 1077 + secret_encrypted, 1078 + encryption_version 1079 + ) 1080 + .execute(&self.pool) 1081 + .await 1082 + .map_err(map_sqlx_error)?; 1083 + 1084 + Ok(()) 1085 + } 1086 + 1087 + async fn set_totp_verified(&self, did: &Did) -> Result<(), DbError> { 1088 + sqlx::query!( 1089 + "UPDATE user_totp SET verified = true, last_used = NOW() WHERE did = $1", 1090 + did.as_str() 1091 + ) 1092 + .execute(&self.pool) 1093 + .await 1094 + .map_err(map_sqlx_error)?; 1095 + 1096 + Ok(()) 1097 + } 1098 + 1099 + async fn update_totp_last_used(&self, did: &Did) -> Result<(), DbError> { 1100 + sqlx::query!( 1101 + "UPDATE user_totp SET last_used = NOW() WHERE did = $1", 1102 + did.as_str() 1103 + ) 1104 + .execute(&self.pool) 1105 + .await 1106 + .map_err(map_sqlx_error)?; 1107 + 1108 + Ok(()) 1109 + } 1110 + 1111 + async fn delete_totp(&self, did: &Did) -> Result<(), DbError> { 1112 + sqlx::query!("DELETE FROM user_totp WHERE did = $1", did.as_str()) 1113 + .execute(&self.pool) 1114 + .await 1115 + .map_err(map_sqlx_error)?; 1116 + 1117 + Ok(()) 1118 + } 1119 + 1120 + async fn get_unused_backup_codes(&self, did: &Did) -> Result<Vec<StoredBackupCode>, DbError> { 1121 + let rows = sqlx::query!( 1122 + "SELECT id, code_hash FROM backup_codes WHERE did = $1 AND used_at IS NULL", 1123 + did.as_str() 1124 + ) 1125 + .fetch_all(&self.pool) 1126 + .await 1127 + .map_err(map_sqlx_error)?; 1128 + 1129 + Ok(rows 1130 + .into_iter() 1131 + .map(|r| StoredBackupCode { 1132 + id: r.id, 1133 + code_hash: r.code_hash, 1134 + }) 1135 + .collect()) 1136 + } 1137 + 1138 + async fn mark_backup_code_used(&self, code_id: Uuid) -> Result<bool, DbError> { 1139 + let result = sqlx::query!( 1140 + "UPDATE backup_codes SET used_at = NOW() WHERE id = $1", 1141 + code_id 1142 + ) 1143 + .execute(&self.pool) 1144 + .await 1145 + .map_err(map_sqlx_error)?; 1146 + 1147 + Ok(result.rows_affected() > 0) 1148 + } 1149 + 1150 + async fn count_unused_backup_codes(&self, did: &Did) -> Result<i64, DbError> { 1151 + let row = sqlx::query!( 1152 + "SELECT COUNT(*) as count FROM backup_codes WHERE did = $1 AND used_at IS NULL", 1153 + did.as_str() 1154 + ) 1155 + .fetch_one(&self.pool) 1156 + .await 1157 + .map_err(map_sqlx_error)?; 1158 + 1159 + Ok(row.count.unwrap_or(0)) 1160 + } 1161 + 1162 + async fn delete_backup_codes(&self, did: &Did) -> Result<u64, DbError> { 1163 + let result = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", did.as_str()) 1164 + .execute(&self.pool) 1165 + .await 1166 + .map_err(map_sqlx_error)?; 1167 + 1168 + Ok(result.rows_affected()) 1169 + } 1170 + 1171 + async fn insert_backup_codes(&self, did: &Did, code_hashes: &[String]) -> Result<(), DbError> { 1172 + sqlx::query!( 1173 + r#" 1174 + INSERT INTO backup_codes (did, code_hash, created_at) 1175 + SELECT $1, hash, NOW() FROM UNNEST($2::text[]) AS t(hash) 1176 + "#, 1177 + did.as_str(), 1178 + code_hashes 1179 + ) 1180 + .execute(&self.pool) 1181 + .await 1182 + .map_err(map_sqlx_error)?; 1183 + 1184 + Ok(()) 1185 + } 1186 + 1187 + async fn enable_totp_with_backup_codes( 1188 + &self, 1189 + did: &Did, 1190 + code_hashes: &[String], 1191 + ) -> Result<(), DbError> { 1192 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 1193 + 1194 + sqlx::query!( 1195 + "UPDATE user_totp SET verified = true, last_used = NOW() WHERE did = $1", 1196 + did.as_str() 1197 + ) 1198 + .execute(&mut *tx) 1199 + .await 1200 + .map_err(map_sqlx_error)?; 1201 + 1202 + sqlx::query!("DELETE FROM backup_codes WHERE did = $1", did.as_str()) 1203 + .execute(&mut *tx) 1204 + .await 1205 + .map_err(map_sqlx_error)?; 1206 + 1207 + sqlx::query!( 1208 + r#" 1209 + INSERT INTO backup_codes (did, code_hash, created_at) 1210 + SELECT $1, hash, NOW() FROM UNNEST($2::text[]) AS t(hash) 1211 + "#, 1212 + did.as_str(), 1213 + code_hashes 1214 + ) 1215 + .execute(&mut *tx) 1216 + .await 1217 + .map_err(map_sqlx_error)?; 1218 + 1219 + tx.commit().await.map_err(map_sqlx_error)?; 1220 + 1221 + Ok(()) 1222 + } 1223 + 1224 + async fn delete_totp_and_backup_codes(&self, did: &Did) -> Result<(), DbError> { 1225 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 1226 + 1227 + sqlx::query!("DELETE FROM user_totp WHERE did = $1", did.as_str()) 1228 + .execute(&mut *tx) 1229 + .await 1230 + .map_err(map_sqlx_error)?; 1231 + 1232 + sqlx::query!("DELETE FROM backup_codes WHERE did = $1", did.as_str()) 1233 + .execute(&mut *tx) 1234 + .await 1235 + .map_err(map_sqlx_error)?; 1236 + 1237 + tx.commit().await.map_err(map_sqlx_error)?; 1238 + 1239 + Ok(()) 1240 + } 1241 + 1242 + async fn replace_backup_codes(&self, did: &Did, code_hashes: &[String]) -> Result<(), DbError> { 1243 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 1244 + 1245 + sqlx::query!("DELETE FROM backup_codes WHERE did = $1", did.as_str()) 1246 + .execute(&mut *tx) 1247 + .await 1248 + .map_err(map_sqlx_error)?; 1249 + 1250 + sqlx::query!( 1251 + r#" 1252 + INSERT INTO backup_codes (did, code_hash, created_at) 1253 + SELECT $1, hash, NOW() FROM UNNEST($2::text[]) AS t(hash) 1254 + "#, 1255 + did.as_str(), 1256 + code_hashes 1257 + ) 1258 + .execute(&mut *tx) 1259 + .await 1260 + .map_err(map_sqlx_error)?; 1261 + 1262 + tx.commit().await.map_err(map_sqlx_error)?; 1263 + 1264 + Ok(()) 1265 + } 1266 + 1267 + async fn get_login_check_by_handle_or_email( 1268 + &self, 1269 + identifier: &str, 1270 + ) -> Result<Option<UserLoginCheck>, DbError> { 1271 + sqlx::query!( 1272 + "SELECT did, password_hash FROM users WHERE handle = $1 OR email = $1", 1273 + identifier 1274 + ) 1275 + .fetch_optional(&self.pool) 1276 + .await 1277 + .map_err(map_sqlx_error) 1278 + .map(|opt| { 1279 + opt.map(|r| UserLoginCheck { 1280 + did: Did::from(r.did), 1281 + password_hash: r.password_hash, 1282 + }) 1283 + }) 1284 + } 1285 + 1286 + async fn get_login_info_by_handle_or_email( 1287 + &self, 1288 + identifier: &str, 1289 + ) -> Result<Option<UserLoginInfo>, DbError> { 1290 + sqlx::query!( 1291 + r#" 1292 + SELECT id, did, email, password_hash, password_required, two_factor_enabled, 1293 + preferred_comms_channel as "preferred_comms_channel!: CommsChannel", 1294 + deactivated_at, takedown_ref, 1295 + email_verified, discord_verified, telegram_verified, signal_verified, 1296 + account_type::text as "account_type!" 1297 + FROM users 1298 + WHERE handle = $1 OR email = $1 1299 + "#, 1300 + identifier 1301 + ) 1302 + .fetch_optional(&self.pool) 1303 + .await 1304 + .map_err(map_sqlx_error) 1305 + .map(|opt| { 1306 + opt.map(|row| UserLoginInfo { 1307 + id: row.id, 1308 + did: Did::from(row.did), 1309 + email: row.email, 1310 + password_hash: row.password_hash, 1311 + password_required: row.password_required, 1312 + two_factor_enabled: row.two_factor_enabled, 1313 + preferred_comms_channel: row.preferred_comms_channel, 1314 + deactivated_at: row.deactivated_at, 1315 + takedown_ref: row.takedown_ref, 1316 + email_verified: row.email_verified, 1317 + discord_verified: row.discord_verified, 1318 + telegram_verified: row.telegram_verified, 1319 + signal_verified: row.signal_verified, 1320 + account_type: row.account_type, 1321 + }) 1322 + }) 1323 + } 1324 + 1325 + async fn get_2fa_status_by_did(&self, did: &Did) -> Result<Option<User2faStatus>, DbError> { 1326 + sqlx::query!( 1327 + r#" 1328 + SELECT id, two_factor_enabled, 1329 + preferred_comms_channel as "preferred_comms_channel!: CommsChannel", 1330 + email_verified, discord_verified, telegram_verified, signal_verified 1331 + FROM users 1332 + WHERE did = $1 1333 + "#, 1334 + did.as_str() 1335 + ) 1336 + .fetch_optional(&self.pool) 1337 + .await 1338 + .map_err(map_sqlx_error) 1339 + .map(|opt| { 1340 + opt.map(|row| User2faStatus { 1341 + id: row.id, 1342 + two_factor_enabled: row.two_factor_enabled, 1343 + preferred_comms_channel: row.preferred_comms_channel, 1344 + email_verified: row.email_verified, 1345 + discord_verified: row.discord_verified, 1346 + telegram_verified: row.telegram_verified, 1347 + signal_verified: row.signal_verified, 1348 + }) 1349 + }) 1350 + } 1351 + 1352 + async fn get_session_info_by_did( 1353 + &self, 1354 + did: &Did, 1355 + ) -> Result<Option<UserSessionInfo>, DbError> { 1356 + sqlx::query!( 1357 + r#" 1358 + SELECT handle, email, email_verified, is_admin, deactivated_at, takedown_ref, 1359 + preferred_locale, 1360 + preferred_comms_channel as "preferred_comms_channel!: CommsChannel", 1361 + discord_verified, telegram_verified, signal_verified, 1362 + migrated_to_pds, migrated_at 1363 + FROM users 1364 + WHERE did = $1 1365 + "#, 1366 + did.as_str() 1367 + ) 1368 + .fetch_optional(&self.pool) 1369 + .await 1370 + .map_err(map_sqlx_error) 1371 + .map(|opt| { 1372 + opt.map(|row| UserSessionInfo { 1373 + handle: Handle::from(row.handle), 1374 + email: row.email, 1375 + email_verified: row.email_verified, 1376 + is_admin: row.is_admin, 1377 + deactivated_at: row.deactivated_at, 1378 + takedown_ref: row.takedown_ref, 1379 + preferred_locale: row.preferred_locale, 1380 + preferred_comms_channel: row.preferred_comms_channel, 1381 + discord_verified: row.discord_verified, 1382 + telegram_verified: row.telegram_verified, 1383 + signal_verified: row.signal_verified, 1384 + migrated_to_pds: row.migrated_to_pds, 1385 + migrated_at: row.migrated_at, 1386 + }) 1387 + }) 1388 + } 1389 + 1390 + async fn get_legacy_login_pref( 1391 + &self, 1392 + did: &Did, 1393 + ) -> Result<Option<UserLegacyLoginPref>, DbError> { 1394 + sqlx::query!( 1395 + r#" 1396 + SELECT u.allow_legacy_login, 1397 + (EXISTS(SELECT 1 FROM user_totp t WHERE t.did = u.did AND t.verified = TRUE) OR 1398 + EXISTS(SELECT 1 FROM passkeys p WHERE p.did = u.did)) as "has_mfa!" 1399 + FROM users u 1400 + WHERE u.did = $1 1401 + "#, 1402 + did.as_str() 1403 + ) 1404 + .fetch_optional(&self.pool) 1405 + .await 1406 + .map_err(map_sqlx_error) 1407 + .map(|opt| { 1408 + opt.map(|row| UserLegacyLoginPref { 1409 + allow_legacy_login: row.allow_legacy_login, 1410 + has_mfa: row.has_mfa, 1411 + }) 1412 + }) 1413 + } 1414 + 1415 + async fn update_legacy_login(&self, did: &Did, allow: bool) -> Result<bool, DbError> { 1416 + let result = sqlx::query!( 1417 + "UPDATE users SET allow_legacy_login = $1 WHERE did = $2 RETURNING did", 1418 + allow, 1419 + did.as_str() 1420 + ) 1421 + .fetch_optional(&self.pool) 1422 + .await 1423 + .map_err(map_sqlx_error)?; 1424 + Ok(result.is_some()) 1425 + } 1426 + 1427 + async fn update_locale(&self, did: &Did, locale: &str) -> Result<bool, DbError> { 1428 + let result = sqlx::query!( 1429 + "UPDATE users SET preferred_locale = $1 WHERE did = $2 RETURNING did", 1430 + locale, 1431 + did.as_str() 1432 + ) 1433 + .fetch_optional(&self.pool) 1434 + .await 1435 + .map_err(map_sqlx_error)?; 1436 + Ok(result.is_some()) 1437 + } 1438 + 1439 + async fn get_login_full_by_identifier( 1440 + &self, 1441 + identifier: &str, 1442 + ) -> Result<Option<UserLoginFull>, DbError> { 1443 + sqlx::query!( 1444 + r#"SELECT 1445 + u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref, 1446 + u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified, 1447 + u.allow_legacy_login, u.migrated_to_pds, 1448 + u.preferred_comms_channel as "preferred_comms_channel: CommsChannel", 1449 + k.key_bytes, k.encryption_version, 1450 + (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled 1451 + FROM users u 1452 + JOIN user_keys k ON u.id = k.user_id 1453 + WHERE u.handle = $1 OR u.email = $1 OR u.did = $1"#, 1454 + identifier 1455 + ) 1456 + .fetch_optional(&self.pool) 1457 + .await 1458 + .map_err(map_sqlx_error) 1459 + .map(|opt| { 1460 + opt.map(|row| UserLoginFull { 1461 + id: row.id, 1462 + did: Did::from(row.did), 1463 + handle: Handle::from(row.handle), 1464 + password_hash: row.password_hash, 1465 + email: row.email, 1466 + deactivated_at: row.deactivated_at, 1467 + takedown_ref: row.takedown_ref, 1468 + email_verified: row.email_verified, 1469 + discord_verified: row.discord_verified, 1470 + telegram_verified: row.telegram_verified, 1471 + signal_verified: row.signal_verified, 1472 + allow_legacy_login: row.allow_legacy_login, 1473 + migrated_to_pds: row.migrated_to_pds, 1474 + preferred_comms_channel: row.preferred_comms_channel, 1475 + key_bytes: row.key_bytes, 1476 + encryption_version: row.encryption_version, 1477 + totp_enabled: row.totp_enabled.unwrap_or(false), 1478 + }) 1479 + }) 1480 + } 1481 + 1482 + async fn get_confirm_signup_by_did( 1483 + &self, 1484 + did: &Did, 1485 + ) -> Result<Option<UserConfirmSignup>, DbError> { 1486 + sqlx::query!( 1487 + r#"SELECT 1488 + u.id, u.did, u.handle, u.email, 1489 + u.preferred_comms_channel as "channel: CommsChannel", 1490 + u.discord_id, u.telegram_username, u.signal_number, 1491 + k.key_bytes, k.encryption_version 1492 + FROM users u 1493 + JOIN user_keys k ON u.id = k.user_id 1494 + WHERE u.did = $1"#, 1495 + did.as_str() 1496 + ) 1497 + .fetch_optional(&self.pool) 1498 + .await 1499 + .map_err(map_sqlx_error) 1500 + .map(|opt| { 1501 + opt.map(|row| UserConfirmSignup { 1502 + id: row.id, 1503 + did: Did::from(row.did), 1504 + handle: Handle::from(row.handle), 1505 + email: row.email, 1506 + channel: row.channel, 1507 + discord_id: row.discord_id, 1508 + telegram_username: row.telegram_username, 1509 + signal_number: row.signal_number, 1510 + key_bytes: row.key_bytes, 1511 + encryption_version: row.encryption_version, 1512 + }) 1513 + }) 1514 + } 1515 + 1516 + async fn get_resend_verification_by_did( 1517 + &self, 1518 + did: &Did, 1519 + ) -> Result<Option<UserResendVerification>, DbError> { 1520 + sqlx::query!( 1521 + r#"SELECT 1522 + id, handle, email, 1523 + preferred_comms_channel as "channel: CommsChannel", 1524 + discord_id, telegram_username, signal_number, 1525 + email_verified, discord_verified, telegram_verified, signal_verified 1526 + FROM users 1527 + WHERE did = $1"#, 1528 + did.as_str() 1529 + ) 1530 + .fetch_optional(&self.pool) 1531 + .await 1532 + .map_err(map_sqlx_error) 1533 + .map(|opt| { 1534 + opt.map(|row| UserResendVerification { 1535 + id: row.id, 1536 + handle: Handle::from(row.handle), 1537 + email: row.email, 1538 + channel: row.channel, 1539 + discord_id: row.discord_id, 1540 + telegram_username: row.telegram_username, 1541 + signal_number: row.signal_number, 1542 + email_verified: row.email_verified, 1543 + discord_verified: row.discord_verified, 1544 + telegram_verified: row.telegram_verified, 1545 + signal_verified: row.signal_verified, 1546 + }) 1547 + }) 1548 + } 1549 + 1550 + async fn set_channel_verified(&self, did: &Did, channel: CommsChannel) -> Result<(), DbError> { 1551 + let column = match channel { 1552 + CommsChannel::Email => "email_verified", 1553 + CommsChannel::Discord => "discord_verified", 1554 + CommsChannel::Telegram => "telegram_verified", 1555 + CommsChannel::Signal => "signal_verified", 1556 + }; 1557 + let query = format!("UPDATE users SET {} = TRUE WHERE did = $1", column); 1558 + sqlx::query(&query) 1559 + .bind(did.as_str()) 1560 + .execute(&self.pool) 1561 + .await 1562 + .map_err(map_sqlx_error)?; 1563 + Ok(()) 1564 + } 1565 + 1566 + async fn get_id_by_email_or_handle( 1567 + &self, 1568 + email: &str, 1569 + handle: &str, 1570 + ) -> Result<Option<Uuid>, DbError> { 1571 + sqlx::query_scalar!( 1572 + "SELECT id FROM users WHERE LOWER(email) = $1 OR handle = $2", 1573 + email, 1574 + handle 1575 + ) 1576 + .fetch_optional(&self.pool) 1577 + .await 1578 + .map_err(map_sqlx_error) 1579 + } 1580 + 1581 + async fn set_password_reset_code( 1582 + &self, 1583 + user_id: Uuid, 1584 + code: &str, 1585 + expires_at: DateTime<Utc>, 1586 + ) -> Result<(), DbError> { 1587 + sqlx::query!( 1588 + "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", 1589 + code, 1590 + expires_at, 1591 + user_id 1592 + ) 1593 + .execute(&self.pool) 1594 + .await 1595 + .map_err(map_sqlx_error)?; 1596 + Ok(()) 1597 + } 1598 + 1599 + async fn get_user_by_reset_code( 1600 + &self, 1601 + code: &str, 1602 + ) -> Result<Option<UserResetCodeInfo>, DbError> { 1603 + sqlx::query!( 1604 + "SELECT id, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 1605 + code 1606 + ) 1607 + .fetch_optional(&self.pool) 1608 + .await 1609 + .map_err(map_sqlx_error) 1610 + .map(|opt| { 1611 + opt.map(|row| UserResetCodeInfo { 1612 + id: row.id, 1613 + expires_at: row.password_reset_code_expires_at, 1614 + }) 1615 + }) 1616 + } 1617 + 1618 + async fn clear_password_reset_code(&self, user_id: Uuid) -> Result<(), DbError> { 1619 + sqlx::query!( 1620 + "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 1621 + user_id 1622 + ) 1623 + .execute(&self.pool) 1624 + .await 1625 + .map_err(map_sqlx_error)?; 1626 + Ok(()) 1627 + } 1628 + 1629 + async fn get_id_and_password_hash_by_did( 1630 + &self, 1631 + did: &Did, 1632 + ) -> Result<Option<UserIdAndPasswordHash>, DbError> { 1633 + sqlx::query!( 1634 + "SELECT id, password_hash FROM users WHERE did = $1", 1635 + did.as_str() 1636 + ) 1637 + .fetch_optional(&self.pool) 1638 + .await 1639 + .map_err(map_sqlx_error) 1640 + .map(|opt| { 1641 + opt.and_then(|row| { 1642 + row.password_hash.map(|hash| UserIdAndPasswordHash { 1643 + id: row.id, 1644 + password_hash: hash, 1645 + }) 1646 + }) 1647 + }) 1648 + } 1649 + 1650 + async fn update_password_hash(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError> { 1651 + sqlx::query!( 1652 + "UPDATE users SET password_hash = $1 WHERE id = $2", 1653 + password_hash, 1654 + user_id 1655 + ) 1656 + .execute(&self.pool) 1657 + .await 1658 + .map_err(map_sqlx_error)?; 1659 + Ok(()) 1660 + } 1661 + 1662 + async fn reset_password_with_sessions( 1663 + &self, 1664 + user_id: Uuid, 1665 + password_hash: &str, 1666 + ) -> Result<PasswordResetResult, DbError> { 1667 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 1668 + 1669 + sqlx::query!( 1670 + "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL, password_required = TRUE WHERE id = $2", 1671 + password_hash, 1672 + user_id 1673 + ) 1674 + .execute(&mut *tx) 1675 + .await 1676 + .map_err(map_sqlx_error)?; 1677 + 1678 + let user_did = sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", user_id) 1679 + .fetch_one(&mut *tx) 1680 + .await 1681 + .map_err(map_sqlx_error)?; 1682 + 1683 + let session_jtis: Vec<String> = sqlx::query_scalar!( 1684 + "SELECT access_jti FROM session_tokens WHERE did = $1", 1685 + user_did 1686 + ) 1687 + .fetch_all(&mut *tx) 1688 + .await 1689 + .map_err(map_sqlx_error)?; 1690 + 1691 + sqlx::query!("DELETE FROM session_tokens WHERE did = $1", user_did) 1692 + .execute(&mut *tx) 1693 + .await 1694 + .map_err(map_sqlx_error)?; 1695 + 1696 + tx.commit().await.map_err(map_sqlx_error)?; 1697 + 1698 + Ok(PasswordResetResult { 1699 + did: Did::from(user_did), 1700 + session_jtis, 1701 + }) 1702 + } 1703 + 1704 + async fn activate_account(&self, did: &Did) -> Result<bool, DbError> { 1705 + let result = sqlx::query!( 1706 + "UPDATE users SET deactivated_at = NULL WHERE did = $1", 1707 + did.as_str() 1708 + ) 1709 + .execute(&self.pool) 1710 + .await 1711 + .map_err(map_sqlx_error)?; 1712 + Ok(result.rows_affected() > 0) 1713 + } 1714 + 1715 + async fn deactivate_account( 1716 + &self, 1717 + did: &Did, 1718 + delete_after: Option<DateTime<Utc>>, 1719 + ) -> Result<bool, DbError> { 1720 + let result = sqlx::query!( 1721 + "UPDATE users SET deactivated_at = NOW(), delete_after = $2 WHERE did = $1", 1722 + did.as_str(), 1723 + delete_after 1724 + ) 1725 + .execute(&self.pool) 1726 + .await 1727 + .map_err(map_sqlx_error)?; 1728 + Ok(result.rows_affected() > 0) 1729 + } 1730 + 1731 + async fn has_password_by_did(&self, did: &Did) -> Result<Option<bool>, DbError> { 1732 + sqlx::query_scalar!( 1733 + "SELECT password_hash IS NOT NULL as has_password FROM users WHERE did = $1", 1734 + did.as_str() 1735 + ) 1736 + .fetch_optional(&self.pool) 1737 + .await 1738 + .map_err(map_sqlx_error) 1739 + .map(|opt| opt.flatten()) 1740 + } 1741 + 1742 + async fn get_password_info_by_did( 1743 + &self, 1744 + did: &Did, 1745 + ) -> Result<Option<UserPasswordInfo>, DbError> { 1746 + sqlx::query!( 1747 + "SELECT id, password_hash FROM users WHERE did = $1", 1748 + did.as_str() 1749 + ) 1750 + .fetch_optional(&self.pool) 1751 + .await 1752 + .map_err(map_sqlx_error) 1753 + .map(|opt| { 1754 + opt.map(|row| UserPasswordInfo { 1755 + id: row.id, 1756 + password_hash: row.password_hash, 1757 + }) 1758 + }) 1759 + } 1760 + 1761 + async fn remove_user_password(&self, user_id: Uuid) -> Result<(), DbError> { 1762 + sqlx::query!( 1763 + "UPDATE users SET password_hash = NULL, password_required = FALSE WHERE id = $1", 1764 + user_id 1765 + ) 1766 + .execute(&self.pool) 1767 + .await 1768 + .map_err(map_sqlx_error)?; 1769 + Ok(()) 1770 + } 1771 + 1772 + async fn set_new_user_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DbError> { 1773 + sqlx::query!( 1774 + "UPDATE users SET password_hash = $1, password_required = TRUE WHERE id = $2", 1775 + password_hash, 1776 + user_id 1777 + ) 1778 + .execute(&self.pool) 1779 + .await 1780 + .map_err(map_sqlx_error)?; 1781 + Ok(()) 1782 + } 1783 + 1784 + async fn is_account_active_by_did(&self, did: &Did) -> Result<Option<bool>, DbError> { 1785 + sqlx::query_scalar!( 1786 + "SELECT deactivated_at IS NULL as is_active FROM users WHERE did = $1", 1787 + did.as_str() 1788 + ) 1789 + .fetch_optional(&self.pool) 1790 + .await 1791 + .map_err(map_sqlx_error) 1792 + .map(|opt| opt.flatten()) 1793 + } 1794 + 1795 + async fn get_user_for_deletion(&self, did: &Did) -> Result<Option<UserForDeletion>, DbError> { 1796 + sqlx::query!( 1797 + "SELECT id, password_hash, handle FROM users WHERE did = $1", 1798 + did.as_str() 1799 + ) 1800 + .fetch_optional(&self.pool) 1801 + .await 1802 + .map_err(map_sqlx_error) 1803 + .map(|opt| { 1804 + opt.map(|row| UserForDeletion { 1805 + id: row.id, 1806 + password_hash: row.password_hash, 1807 + handle: Handle::from(row.handle), 1808 + }) 1809 + }) 1810 + } 1811 + 1812 + async fn get_user_key_by_did(&self, did: &Did) -> Result<Option<UserKeyInfo>, DbError> { 1813 + sqlx::query!( 1814 + r#"SELECT uk.key_bytes, uk.encryption_version 1815 + FROM user_keys uk 1816 + JOIN users u ON uk.user_id = u.id 1817 + WHERE u.did = $1"#, 1818 + did.as_str() 1819 + ) 1820 + .fetch_optional(&self.pool) 1821 + .await 1822 + .map_err(map_sqlx_error) 1823 + .map(|opt| { 1824 + opt.map(|row| UserKeyInfo { 1825 + key_bytes: row.key_bytes, 1826 + encryption_version: row.encryption_version, 1827 + }) 1828 + }) 1829 + } 1830 + 1831 + async fn delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError> { 1832 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 1833 + sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did.as_str()) 1834 + .execute(&mut *tx) 1835 + .await 1836 + .map_err(map_sqlx_error)?; 1837 + sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id) 1838 + .execute(&mut *tx) 1839 + .await 1840 + .map_err(map_sqlx_error)?; 1841 + sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id) 1842 + .execute(&mut *tx) 1843 + .await 1844 + .map_err(map_sqlx_error)?; 1845 + sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) 1846 + .execute(&mut *tx) 1847 + .await 1848 + .map_err(map_sqlx_error)?; 1849 + sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id) 1850 + .execute(&mut *tx) 1851 + .await 1852 + .map_err(map_sqlx_error)?; 1853 + sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id) 1854 + .execute(&mut *tx) 1855 + .await 1856 + .map_err(map_sqlx_error)?; 1857 + sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did.as_str()) 1858 + .execute(&mut *tx) 1859 + .await 1860 + .map_err(map_sqlx_error)?; 1861 + sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 1862 + .execute(&mut *tx) 1863 + .await 1864 + .map_err(map_sqlx_error)?; 1865 + tx.commit().await.map_err(map_sqlx_error)?; 1866 + Ok(()) 1867 + } 1868 + 1869 + async fn set_user_takedown(&self, did: &Did, takedown_ref: Option<&str>) -> Result<bool, DbError> { 1870 + let result = sqlx::query!( 1871 + "UPDATE users SET takedown_ref = $1 WHERE did = $2", 1872 + takedown_ref, 1873 + did.as_str() 1874 + ) 1875 + .execute(&self.pool) 1876 + .await 1877 + .map_err(map_sqlx_error)?; 1878 + Ok(result.rows_affected() > 0) 1879 + } 1880 + 1881 + async fn admin_delete_account_complete(&self, user_id: Uuid, did: &Did) -> Result<(), DbError> { 1882 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 1883 + sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did.as_str()) 1884 + .execute(&mut *tx) 1885 + .await 1886 + .map_err(map_sqlx_error)?; 1887 + sqlx::query!( 1888 + "DELETE FROM used_refresh_tokens WHERE session_id IN (SELECT id FROM session_tokens WHERE did = $1)", 1889 + did.as_str() 1890 + ) 1891 + .execute(&mut *tx) 1892 + .await 1893 + .ok(); 1894 + sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id) 1895 + .execute(&mut *tx) 1896 + .await 1897 + .map_err(map_sqlx_error)?; 1898 + sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id) 1899 + .execute(&mut *tx) 1900 + .await 1901 + .map_err(map_sqlx_error)?; 1902 + sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) 1903 + .execute(&mut *tx) 1904 + .await 1905 + .map_err(map_sqlx_error)?; 1906 + sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id) 1907 + .execute(&mut *tx) 1908 + .await 1909 + .map_err(map_sqlx_error)?; 1910 + sqlx::query!( 1911 + "DELETE FROM invite_code_uses WHERE used_by_user = $1", 1912 + user_id 1913 + ) 1914 + .execute(&mut *tx) 1915 + .await 1916 + .ok(); 1917 + sqlx::query!( 1918 + "DELETE FROM invite_codes WHERE created_by_user = $1", 1919 + user_id 1920 + ) 1921 + .execute(&mut *tx) 1922 + .await 1923 + .ok(); 1924 + sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id) 1925 + .execute(&mut *tx) 1926 + .await 1927 + .map_err(map_sqlx_error)?; 1928 + sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 1929 + .execute(&mut *tx) 1930 + .await 1931 + .map_err(map_sqlx_error)?; 1932 + tx.commit().await.map_err(map_sqlx_error)?; 1933 + Ok(()) 1934 + } 1935 + 1936 + async fn get_user_for_did_doc(&self, did: &Did) -> Result<Option<UserForDidDoc>, DbError> { 1937 + let row = sqlx::query!( 1938 + "SELECT id, handle, deactivated_at FROM users WHERE did = $1", 1939 + did.as_str() 1940 + ) 1941 + .fetch_optional(&self.pool) 1942 + .await 1943 + .map_err(map_sqlx_error)?; 1944 + 1945 + Ok(row.map(|r| UserForDidDoc { 1946 + id: r.id, 1947 + handle: Handle::from(r.handle), 1948 + deactivated_at: r.deactivated_at, 1949 + })) 1950 + } 1951 + 1952 + async fn get_user_for_did_doc_build(&self, did: &Did) -> Result<Option<UserForDidDocBuild>, DbError> { 1953 + let row = sqlx::query!( 1954 + "SELECT id, handle, migrated_to_pds FROM users WHERE did = $1", 1955 + did.as_str() 1956 + ) 1957 + .fetch_optional(&self.pool) 1958 + .await 1959 + .map_err(map_sqlx_error)?; 1960 + 1961 + Ok(row.map(|r| UserForDidDocBuild { 1962 + id: r.id, 1963 + handle: Handle::from(r.handle), 1964 + migrated_to_pds: r.migrated_to_pds, 1965 + })) 1966 + } 1967 + 1968 + async fn upsert_did_web_overrides( 1969 + &self, 1970 + user_id: Uuid, 1971 + verification_methods: Option<serde_json::Value>, 1972 + also_known_as: Option<Vec<String>>, 1973 + ) -> Result<(), DbError> { 1974 + let now = chrono::Utc::now(); 1975 + sqlx::query!( 1976 + r#" 1977 + INSERT INTO did_web_overrides (user_id, verification_methods, also_known_as, updated_at) 1978 + VALUES ($1, COALESCE($2, '[]'::jsonb), COALESCE($3, '{}'::text[]), $4) 1979 + ON CONFLICT (user_id) DO UPDATE SET 1980 + verification_methods = CASE WHEN $2 IS NOT NULL THEN $2 ELSE did_web_overrides.verification_methods END, 1981 + also_known_as = CASE WHEN $3 IS NOT NULL THEN $3 ELSE did_web_overrides.also_known_as END, 1982 + updated_at = $4 1983 + "#, 1984 + user_id, 1985 + verification_methods, 1986 + also_known_as.as_deref(), 1987 + now 1988 + ) 1989 + .execute(&self.pool) 1990 + .await 1991 + .map_err(map_sqlx_error)?; 1992 + Ok(()) 1993 + } 1994 + 1995 + async fn update_migrated_to_pds(&self, did: &Did, endpoint: &str) -> Result<(), DbError> { 1996 + let now = chrono::Utc::now(); 1997 + sqlx::query!( 1998 + "UPDATE users SET migrated_to_pds = $1, migrated_at = $2 WHERE did = $3", 1999 + endpoint, 2000 + now, 2001 + did.as_str() 2002 + ) 2003 + .execute(&self.pool) 2004 + .await 2005 + .map_err(map_sqlx_error)?; 2006 + Ok(()) 2007 + } 2008 + 2009 + async fn get_user_for_passkey_setup(&self, did: &Did) -> Result<Option<UserForPasskeySetup>, DbError> { 2010 + let row = sqlx::query!( 2011 + r#"SELECT id, handle, recovery_token, recovery_token_expires_at, password_required 2012 + FROM users WHERE did = $1"#, 2013 + did.as_str() 2014 + ) 2015 + .fetch_optional(&self.pool) 2016 + .await 2017 + .map_err(map_sqlx_error)?; 2018 + 2019 + Ok(row.map(|r| UserForPasskeySetup { 2020 + id: r.id, 2021 + handle: Handle::from(r.handle), 2022 + recovery_token: r.recovery_token, 2023 + recovery_token_expires_at: r.recovery_token_expires_at, 2024 + password_required: r.password_required, 2025 + })) 2026 + } 2027 + 2028 + async fn get_user_for_passkey_recovery( 2029 + &self, 2030 + identifier: &str, 2031 + normalized_handle: &str, 2032 + ) -> Result<Option<UserForPasskeyRecovery>, DbError> { 2033 + let row = sqlx::query!( 2034 + "SELECT id, did, handle, password_required FROM users WHERE LOWER(email) = $1 OR handle = $2", 2035 + identifier, 2036 + normalized_handle 2037 + ) 2038 + .fetch_optional(&self.pool) 2039 + .await 2040 + .map_err(map_sqlx_error)?; 2041 + 2042 + Ok(row.map(|r| UserForPasskeyRecovery { 2043 + id: r.id, 2044 + did: Did::from(r.did), 2045 + handle: Handle::from(r.handle), 2046 + password_required: r.password_required, 2047 + })) 2048 + } 2049 + 2050 + async fn set_recovery_token( 2051 + &self, 2052 + did: &Did, 2053 + token_hash: &str, 2054 + expires_at: DateTime<Utc>, 2055 + ) -> Result<(), DbError> { 2056 + sqlx::query!( 2057 + "UPDATE users SET recovery_token = $1, recovery_token_expires_at = $2 WHERE did = $3", 2058 + token_hash, 2059 + expires_at, 2060 + did.as_str() 2061 + ) 2062 + .execute(&self.pool) 2063 + .await 2064 + .map_err(map_sqlx_error)?; 2065 + Ok(()) 2066 + } 2067 + 2068 + async fn get_user_for_recovery(&self, did: &Did) -> Result<Option<UserForRecovery>, DbError> { 2069 + let row = sqlx::query!( 2070 + "SELECT id, did, recovery_token, recovery_token_expires_at FROM users WHERE did = $1", 2071 + did.as_str() 2072 + ) 2073 + .fetch_optional(&self.pool) 2074 + .await 2075 + .map_err(map_sqlx_error)?; 2076 + 2077 + Ok(row.map(|r| UserForRecovery { 2078 + id: r.id, 2079 + did: Did::from(r.did), 2080 + recovery_token: r.recovery_token, 2081 + recovery_token_expires_at: r.recovery_token_expires_at, 2082 + })) 2083 + } 2084 + 2085 + async fn get_accounts_scheduled_for_deletion( 2086 + &self, 2087 + limit: i64, 2088 + ) -> Result<Vec<tranquil_db_traits::ScheduledDeletionAccount>, DbError> { 2089 + let rows = sqlx::query!( 2090 + r#" 2091 + SELECT id, did, handle 2092 + FROM users 2093 + WHERE delete_after IS NOT NULL 2094 + AND delete_after < NOW() 2095 + AND deactivated_at IS NOT NULL 2096 + LIMIT $1 2097 + "#, 2098 + limit 2099 + ) 2100 + .fetch_all(&self.pool) 2101 + .await 2102 + .map_err(map_sqlx_error)?; 2103 + 2104 + Ok(rows 2105 + .into_iter() 2106 + .map(|r| tranquil_db_traits::ScheduledDeletionAccount { 2107 + id: r.id, 2108 + did: Did::from(r.did), 2109 + handle: Handle::from(r.handle), 2110 + }) 2111 + .collect()) 2112 + } 2113 + 2114 + async fn delete_account_with_firehose( 2115 + &self, 2116 + user_id: Uuid, 2117 + did: &Did, 2118 + ) -> Result<i64, DbError> { 2119 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 2120 + 2121 + sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) 2122 + .execute(&mut *tx) 2123 + .await 2124 + .map_err(map_sqlx_error)?; 2125 + 2126 + sqlx::query!("DELETE FROM record_blobs WHERE repo_id = $1", user_id) 2127 + .execute(&mut *tx) 2128 + .await 2129 + .map_err(map_sqlx_error)?; 2130 + 2131 + sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id) 2132 + .execute(&mut *tx) 2133 + .await 2134 + .map_err(map_sqlx_error)?; 2135 + 2136 + sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id) 2137 + .execute(&mut *tx) 2138 + .await 2139 + .map_err(map_sqlx_error)?; 2140 + 2141 + sqlx::query!("DELETE FROM user_blocks WHERE user_id = $1", user_id) 2142 + .execute(&mut *tx) 2143 + .await 2144 + .map_err(map_sqlx_error)?; 2145 + 2146 + sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id) 2147 + .execute(&mut *tx) 2148 + .await 2149 + .map_err(map_sqlx_error)?; 2150 + 2151 + sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did.as_str()) 2152 + .execute(&mut *tx) 2153 + .await 2154 + .map_err(map_sqlx_error)?; 2155 + 2156 + sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id) 2157 + .execute(&mut *tx) 2158 + .await 2159 + .map_err(map_sqlx_error)?; 2160 + 2161 + sqlx::query!("DELETE FROM passkeys WHERE did = $1", did.as_str()) 2162 + .execute(&mut *tx) 2163 + .await 2164 + .map_err(map_sqlx_error)?; 2165 + 2166 + sqlx::query!("DELETE FROM user_totp WHERE did = $1", did.as_str()) 2167 + .execute(&mut *tx) 2168 + .await 2169 + .map_err(map_sqlx_error)?; 2170 + 2171 + sqlx::query!("DELETE FROM backup_codes WHERE did = $1", did.as_str()) 2172 + .execute(&mut *tx) 2173 + .await 2174 + .map_err(map_sqlx_error)?; 2175 + 2176 + sqlx::query!("DELETE FROM webauthn_challenges WHERE did = $1", did.as_str()) 2177 + .execute(&mut *tx) 2178 + .await 2179 + .map_err(map_sqlx_error)?; 2180 + 2181 + sqlx::query!("DELETE FROM account_backups WHERE user_id = $1", user_id) 2182 + .execute(&mut *tx) 2183 + .await 2184 + .map_err(map_sqlx_error)?; 2185 + 2186 + sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did.as_str()) 2187 + .execute(&mut *tx) 2188 + .await 2189 + .map_err(map_sqlx_error)?; 2190 + 2191 + sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 2192 + .execute(&mut *tx) 2193 + .await 2194 + .map_err(map_sqlx_error)?; 2195 + 2196 + let account_seq: i64 = sqlx::query_scalar!( 2197 + r#" 2198 + INSERT INTO repo_seq (did, event_type, active, status) 2199 + VALUES ($1, 'account', false, 'deleted') 2200 + RETURNING seq 2201 + "#, 2202 + did.as_str() 2203 + ) 2204 + .fetch_one(&mut *tx) 2205 + .await 2206 + .map_err(map_sqlx_error)?; 2207 + 2208 + sqlx::query!( 2209 + "DELETE FROM repo_seq WHERE did = $1 AND seq != $2", 2210 + did.as_str(), 2211 + account_seq 2212 + ) 2213 + .execute(&mut *tx) 2214 + .await 2215 + .map_err(map_sqlx_error)?; 2216 + 2217 + tx.commit().await.map_err(map_sqlx_error)?; 2218 + 2219 + sqlx::query(&format!("NOTIFY repo_updates, '{}'", account_seq)) 2220 + .execute(&self.pool) 2221 + .await 2222 + .map_err(map_sqlx_error)?; 2223 + 2224 + Ok(account_seq) 2225 + } 2226 + 2227 + async fn create_password_account( 2228 + &self, 2229 + input: &tranquil_db_traits::CreatePasswordAccountInput, 2230 + ) -> Result< 2231 + tranquil_db_traits::CreatePasswordAccountResult, 2232 + tranquil_db_traits::CreateAccountError, 2233 + > { 2234 + let mut tx = self 2235 + .pool 2236 + .begin() 2237 + .await 2238 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2239 + 2240 + let is_first_user: bool = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users") 2241 + .fetch_one(&mut *tx) 2242 + .await 2243 + .map(|c| c.unwrap_or(0) == 0) 2244 + .unwrap_or(false); 2245 + 2246 + let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as( 2247 + r#"INSERT INTO users ( 2248 + handle, email, did, password_hash, 2249 + preferred_comms_channel, 2250 + discord_id, telegram_username, signal_number, 2251 + is_admin, deactivated_at, email_verified 2252 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, FALSE) RETURNING id"#, 2253 + ) 2254 + .bind(input.handle.as_str()) 2255 + .bind(&input.email) 2256 + .bind(input.did.as_str()) 2257 + .bind(&input.password_hash) 2258 + .bind(input.preferred_comms_channel) 2259 + .bind(&input.discord_id) 2260 + .bind(&input.telegram_username) 2261 + .bind(&input.signal_number) 2262 + .bind(is_first_user) 2263 + .bind(input.deactivated_at) 2264 + .fetch_one(&mut *tx) 2265 + .await; 2266 + 2267 + let user_id = match user_insert { 2268 + Ok((id,)) => id, 2269 + Err(e) => { 2270 + if let Some(db_err) = e.as_database_error() 2271 + && db_err.code().as_deref() == Some("23505") 2272 + { 2273 + let constraint = db_err.constraint().unwrap_or(""); 2274 + if constraint.contains("handle") { 2275 + return Err(tranquil_db_traits::CreateAccountError::HandleTaken); 2276 + } else if constraint.contains("email") { 2277 + return Err(tranquil_db_traits::CreateAccountError::EmailTaken); 2278 + } else if constraint.contains("did") { 2279 + return Err(tranquil_db_traits::CreateAccountError::DidExists); 2280 + } 2281 + } 2282 + return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string())); 2283 + } 2284 + }; 2285 + 2286 + sqlx::query!( 2287 + "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())", 2288 + user_id, 2289 + &input.encrypted_key_bytes[..], 2290 + input.encryption_version 2291 + ) 2292 + .execute(&mut *tx) 2293 + .await 2294 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2295 + 2296 + if let Some(key_id) = input.reserved_key_id { 2297 + sqlx::query!( 2298 + "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1", 2299 + key_id 2300 + ) 2301 + .execute(&mut *tx) 2302 + .await 2303 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2304 + } 2305 + 2306 + sqlx::query!( 2307 + "INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)", 2308 + user_id, 2309 + input.commit_cid, 2310 + input.repo_rev 2311 + ) 2312 + .execute(&mut *tx) 2313 + .await 2314 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2315 + 2316 + sqlx::query( 2317 + r#" 2318 + INSERT INTO user_blocks (user_id, block_cid, repo_rev) 2319 + SELECT $1, block_cid, $3 FROM UNNEST($2::bytea[]) AS t(block_cid) 2320 + ON CONFLICT (user_id, block_cid) DO NOTHING 2321 + "#, 2322 + ) 2323 + .bind(user_id) 2324 + .bind(&input.genesis_block_cids) 2325 + .bind(&input.repo_rev) 2326 + .execute(&mut *tx) 2327 + .await 2328 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2329 + 2330 + if let Some(code) = &input.invite_code { 2331 + let _ = sqlx::query!( 2332 + "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 2333 + code 2334 + ) 2335 + .execute(&mut *tx) 2336 + .await; 2337 + 2338 + let _ = sqlx::query!( 2339 + "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", 2340 + code, 2341 + user_id 2342 + ) 2343 + .execute(&mut *tx) 2344 + .await; 2345 + } 2346 + 2347 + if let Some(birthdate_pref) = &input.birthdate_pref { 2348 + let _ = sqlx::query!( 2349 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 2350 + ON CONFLICT (user_id, name) DO NOTHING", 2351 + user_id, 2352 + "app.bsky.actor.defs#personalDetailsPref", 2353 + birthdate_pref 2354 + ) 2355 + .execute(&mut *tx) 2356 + .await; 2357 + } 2358 + 2359 + tx.commit() 2360 + .await 2361 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2362 + 2363 + Ok(tranquil_db_traits::CreatePasswordAccountResult { 2364 + user_id, 2365 + is_admin: is_first_user, 2366 + }) 2367 + } 2368 + 2369 + async fn create_delegated_account( 2370 + &self, 2371 + input: &tranquil_db_traits::CreateDelegatedAccountInput, 2372 + ) -> Result<uuid::Uuid, tranquil_db_traits::CreateAccountError> { 2373 + let mut tx = self 2374 + .pool 2375 + .begin() 2376 + .await 2377 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2378 + 2379 + let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as( 2380 + r#"INSERT INTO users ( 2381 + handle, email, did, password_hash, password_required, 2382 + account_type, preferred_comms_channel 2383 + ) VALUES ($1, $2, $3, NULL, FALSE, 'delegated'::account_type, 'email'::comms_channel) RETURNING id"#, 2384 + ) 2385 + .bind(input.handle.as_str()) 2386 + .bind(&input.email) 2387 + .bind(input.did.as_str()) 2388 + .fetch_one(&mut *tx) 2389 + .await; 2390 + 2391 + let user_id = match user_insert { 2392 + Ok((id,)) => id, 2393 + Err(e) => { 2394 + if let Some(db_err) = e.as_database_error() 2395 + && db_err.code().as_deref() == Some("23505") 2396 + { 2397 + let constraint = db_err.constraint().unwrap_or(""); 2398 + if constraint.contains("handle") { 2399 + return Err(tranquil_db_traits::CreateAccountError::HandleTaken); 2400 + } else if constraint.contains("email") { 2401 + return Err(tranquil_db_traits::CreateAccountError::EmailTaken); 2402 + } 2403 + } 2404 + return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string())); 2405 + } 2406 + }; 2407 + 2408 + sqlx::query!( 2409 + "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())", 2410 + user_id, 2411 + &input.encrypted_key_bytes[..], 2412 + input.encryption_version 2413 + ) 2414 + .execute(&mut *tx) 2415 + .await 2416 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2417 + 2418 + sqlx::query!( 2419 + r#"INSERT INTO account_delegations (delegated_did, controller_did, granted_scopes, granted_by) 2420 + VALUES ($1, $2, $3, $4)"#, 2421 + input.did.as_str(), 2422 + input.controller_did.as_str(), 2423 + &input.controller_scopes, 2424 + input.controller_did.as_str() 2425 + ) 2426 + .execute(&mut *tx) 2427 + .await 2428 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2429 + 2430 + sqlx::query!( 2431 + "INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)", 2432 + user_id, 2433 + input.commit_cid, 2434 + input.repo_rev 2435 + ) 2436 + .execute(&mut *tx) 2437 + .await 2438 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2439 + 2440 + sqlx::query( 2441 + r#" 2442 + INSERT INTO user_blocks (user_id, block_cid, repo_rev) 2443 + SELECT $1, block_cid, $3 FROM UNNEST($2::bytea[]) AS t(block_cid) 2444 + ON CONFLICT (user_id, block_cid) DO NOTHING 2445 + "#, 2446 + ) 2447 + .bind(user_id) 2448 + .bind(&input.genesis_block_cids) 2449 + .bind(&input.repo_rev) 2450 + .execute(&mut *tx) 2451 + .await 2452 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2453 + 2454 + if let Some(code) = &input.invite_code { 2455 + let _ = sqlx::query!( 2456 + "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 2457 + code 2458 + ) 2459 + .execute(&mut *tx) 2460 + .await; 2461 + 2462 + let _ = sqlx::query!( 2463 + "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", 2464 + code, 2465 + user_id 2466 + ) 2467 + .execute(&mut *tx) 2468 + .await; 2469 + } 2470 + 2471 + tx.commit() 2472 + .await 2473 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2474 + 2475 + Ok(user_id) 2476 + } 2477 + 2478 + async fn create_passkey_account( 2479 + &self, 2480 + input: &tranquil_db_traits::CreatePasskeyAccountInput, 2481 + ) -> Result< 2482 + tranquil_db_traits::CreatePasswordAccountResult, 2483 + tranquil_db_traits::CreateAccountError, 2484 + > { 2485 + let mut tx = self 2486 + .pool 2487 + .begin() 2488 + .await 2489 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2490 + 2491 + let is_first_user: bool = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users") 2492 + .fetch_one(&mut *tx) 2493 + .await 2494 + .map(|c| c.unwrap_or(0) == 0) 2495 + .unwrap_or(false); 2496 + 2497 + let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as( 2498 + r#"INSERT INTO users ( 2499 + handle, email, did, password_hash, password_required, 2500 + preferred_comms_channel, 2501 + discord_id, telegram_username, signal_number, 2502 + recovery_token, recovery_token_expires_at, 2503 + is_admin, deactivated_at 2504 + ) VALUES ($1, $2, $3, NULL, FALSE, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id"#, 2505 + ) 2506 + .bind(input.handle.as_str()) 2507 + .bind(&input.email) 2508 + .bind(input.did.as_str()) 2509 + .bind(input.preferred_comms_channel) 2510 + .bind(&input.discord_id) 2511 + .bind(&input.telegram_username) 2512 + .bind(&input.signal_number) 2513 + .bind(&input.setup_token_hash) 2514 + .bind(input.setup_expires_at) 2515 + .bind(is_first_user) 2516 + .bind(input.deactivated_at) 2517 + .fetch_one(&mut *tx) 2518 + .await; 2519 + 2520 + let user_id = match user_insert { 2521 + Ok((id,)) => id, 2522 + Err(e) => { 2523 + if let Some(db_err) = e.as_database_error() 2524 + && db_err.code().as_deref() == Some("23505") 2525 + { 2526 + let constraint = db_err.constraint().unwrap_or(""); 2527 + if constraint.contains("handle") { 2528 + return Err(tranquil_db_traits::CreateAccountError::HandleTaken); 2529 + } else if constraint.contains("email") { 2530 + return Err(tranquil_db_traits::CreateAccountError::EmailTaken); 2531 + } 2532 + } 2533 + return Err(tranquil_db_traits::CreateAccountError::Database(e.to_string())); 2534 + } 2535 + }; 2536 + 2537 + sqlx::query!( 2538 + "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())", 2539 + user_id, 2540 + &input.encrypted_key_bytes[..], 2541 + input.encryption_version 2542 + ) 2543 + .execute(&mut *tx) 2544 + .await 2545 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2546 + 2547 + if let Some(key_id) = input.reserved_key_id { 2548 + sqlx::query!( 2549 + "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1", 2550 + key_id 2551 + ) 2552 + .execute(&mut *tx) 2553 + .await 2554 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2555 + } 2556 + 2557 + sqlx::query!( 2558 + "INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)", 2559 + user_id, 2560 + input.commit_cid, 2561 + input.repo_rev 2562 + ) 2563 + .execute(&mut *tx) 2564 + .await 2565 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2566 + 2567 + sqlx::query( 2568 + r#" 2569 + INSERT INTO user_blocks (user_id, block_cid, repo_rev) 2570 + SELECT $1, block_cid, $3 FROM UNNEST($2::bytea[]) AS t(block_cid) 2571 + ON CONFLICT (user_id, block_cid) DO NOTHING 2572 + "#, 2573 + ) 2574 + .bind(user_id) 2575 + .bind(&input.genesis_block_cids) 2576 + .bind(&input.repo_rev) 2577 + .execute(&mut *tx) 2578 + .await 2579 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2580 + 2581 + if let Some(code) = &input.invite_code { 2582 + let _ = sqlx::query!( 2583 + "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 2584 + code 2585 + ) 2586 + .execute(&mut *tx) 2587 + .await; 2588 + 2589 + let _ = sqlx::query!( 2590 + "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", 2591 + code, 2592 + user_id 2593 + ) 2594 + .execute(&mut *tx) 2595 + .await; 2596 + } 2597 + 2598 + if let Some(birthdate_pref) = &input.birthdate_pref { 2599 + let _ = sqlx::query!( 2600 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 2601 + ON CONFLICT (user_id, name) DO NOTHING", 2602 + user_id, 2603 + "app.bsky.actor.defs#personalDetailsPref", 2604 + birthdate_pref 2605 + ) 2606 + .execute(&mut *tx) 2607 + .await; 2608 + } 2609 + 2610 + tx.commit() 2611 + .await 2612 + .map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?; 2613 + 2614 + Ok(tranquil_db_traits::CreatePasswordAccountResult { 2615 + user_id, 2616 + is_admin: is_first_user, 2617 + }) 2618 + } 2619 + 2620 + async fn reactivate_migration_account( 2621 + &self, 2622 + input: &tranquil_db_traits::MigrationReactivationInput, 2623 + ) -> Result< 2624 + tranquil_db_traits::ReactivatedAccountInfo, 2625 + tranquil_db_traits::MigrationReactivationError, 2626 + > { 2627 + let mut tx = self 2628 + .pool 2629 + .begin() 2630 + .await 2631 + .map_err(|e| tranquil_db_traits::MigrationReactivationError::Database(e.to_string()))?; 2632 + 2633 + let existing: Option<(uuid::Uuid, String, Option<chrono::DateTime<chrono::Utc>>)> = 2634 + sqlx::query_as("SELECT id, handle, deactivated_at FROM users WHERE did = $1 FOR UPDATE") 2635 + .bind(input.did.as_str()) 2636 + .fetch_optional(&mut *tx) 2637 + .await 2638 + .map_err(|e| { 2639 + tranquil_db_traits::MigrationReactivationError::Database(e.to_string()) 2640 + })?; 2641 + 2642 + let (account_id, old_handle, deactivated_at) = existing 2643 + .ok_or(tranquil_db_traits::MigrationReactivationError::NotFound)?; 2644 + 2645 + if deactivated_at.is_none() { 2646 + return Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated); 2647 + } 2648 + 2649 + let update_result: Result<_, sqlx::Error> = 2650 + sqlx::query("UPDATE users SET handle = $1 WHERE id = $2") 2651 + .bind(input.new_handle.as_str()) 2652 + .bind(account_id) 2653 + .execute(&mut *tx) 2654 + .await; 2655 + 2656 + if let Err(e) = update_result { 2657 + if let Some(db_err) = e.as_database_error() 2658 + && db_err 2659 + .constraint() 2660 + .map(|c| c.contains("handle")) 2661 + .unwrap_or(false) 2662 + { 2663 + return Err(tranquil_db_traits::MigrationReactivationError::HandleTaken); 2664 + } 2665 + return Err(tranquil_db_traits::MigrationReactivationError::Database( 2666 + e.to_string(), 2667 + )); 2668 + } 2669 + 2670 + tx.commit() 2671 + .await 2672 + .map_err(|e| tranquil_db_traits::MigrationReactivationError::Database(e.to_string()))?; 2673 + 2674 + Ok(tranquil_db_traits::ReactivatedAccountInfo { 2675 + user_id: account_id, 2676 + old_handle: Handle::from(old_handle), 2677 + }) 2678 + } 2679 + 2680 + async fn check_handle_available_for_new_account(&self, handle: &Handle) -> Result<bool, DbError> { 2681 + let exists: Option<(i32,)> = 2682 + sqlx::query_as("SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL") 2683 + .bind(handle.as_str()) 2684 + .fetch_optional(&self.pool) 2685 + .await 2686 + .map_err(map_sqlx_error)?; 2687 + 2688 + Ok(exists.is_none()) 2689 + } 2690 + 2691 + async fn check_and_consume_invite_code(&self, code: &str) -> Result<bool, DbError> { 2692 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 2693 + 2694 + let invite = sqlx::query!( 2695 + "SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE", 2696 + code 2697 + ) 2698 + .fetch_optional(&mut *tx) 2699 + .await 2700 + .map_err(map_sqlx_error)?; 2701 + 2702 + let Some(row) = invite else { 2703 + return Ok(false); 2704 + }; 2705 + 2706 + if row.available_uses <= 0 { 2707 + return Ok(false); 2708 + } 2709 + 2710 + sqlx::query!( 2711 + "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 2712 + code 2713 + ) 2714 + .execute(&mut *tx) 2715 + .await 2716 + .map_err(map_sqlx_error)?; 2717 + 2718 + tx.commit().await.map_err(map_sqlx_error)?; 2719 + 2720 + Ok(true) 2721 + } 2722 + 2723 + async fn complete_passkey_setup( 2724 + &self, 2725 + input: &tranquil_db_traits::CompletePasskeySetupInput, 2726 + ) -> Result<(), DbError> { 2727 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 2728 + 2729 + sqlx::query!( 2730 + "INSERT INTO app_passwords (user_id, name, password_hash, privileged) VALUES ($1, $2, $3, FALSE)", 2731 + input.user_id, 2732 + input.app_password_name, 2733 + input.app_password_hash 2734 + ) 2735 + .execute(&mut *tx) 2736 + .await 2737 + .map_err(map_sqlx_error)?; 2738 + 2739 + sqlx::query!( 2740 + "UPDATE users SET recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $1", 2741 + input.did.as_str() 2742 + ) 2743 + .execute(&mut *tx) 2744 + .await 2745 + .map_err(map_sqlx_error)?; 2746 + 2747 + tx.commit().await.map_err(map_sqlx_error)?; 2748 + 2749 + Ok(()) 2750 + } 2751 + 2752 + async fn recover_passkey_account( 2753 + &self, 2754 + input: &tranquil_db_traits::RecoverPasskeyAccountInput, 2755 + ) -> Result<tranquil_db_traits::RecoverPasskeyAccountResult, DbError> { 2756 + let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?; 2757 + 2758 + sqlx::query!( 2759 + "UPDATE users SET password_hash = $1, password_required = TRUE, recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $2", 2760 + input.password_hash, 2761 + input.did.as_str() 2762 + ) 2763 + .execute(&mut *tx) 2764 + .await 2765 + .map_err(map_sqlx_error)?; 2766 + 2767 + let deleted = sqlx::query!("DELETE FROM passkeys WHERE did = $1", input.did.as_str()) 2768 + .execute(&mut *tx) 2769 + .await 2770 + .map_err(map_sqlx_error)?; 2771 + 2772 + tx.commit().await.map_err(map_sqlx_error)?; 2773 + 2774 + Ok(tranquil_db_traits::RecoverPasskeyAccountResult { 2775 + passkeys_deleted: deleted.rows_affected(), 2776 + }) 2777 + } 2778 + }