tangled
alpha
login
or
join now
tranquil.farm
/
tranquil-pds
156
fork
atom
Our Personal Data Server from scratch!
tranquil.farm
oauth
atproto
pds
rust
postgresql
objectstorage
fun
156
fork
atom
overview
issues
23
pulls
2
pipelines
tranquil-db crates, repository pattern for db access
lewis.moe
1 month ago
71d9ed7d
0bad085e
+10937
-1
27 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
crates
tranquil-db
Cargo.toml
src
lib.rs
postgres
backlink.rs
backup.rs
blob.rs
delegation.rs
event_notifier.rs
infra.rs
mod.rs
oauth.rs
repo.rs
session.rs
user.rs
tranquil-db-traits
Cargo.toml
src
backlink.rs
backup.rs
blob.rs
delegation.rs
error.rs
infra.rs
lib.rs
oauth.rs
repo.rs
session.rs
user.rs
+37
Cargo.lock
···
6375
"sqlx",
6376
"thiserror 2.0.17",
6377
"tokio",
0
6378
"urlencoding",
6379
"uuid",
6380
]
···
6394
"sha2",
6395
"subtle",
6396
"thiserror 2.0.17",
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
6397
]
6398
6399
[[package]]
···
6503
"tranquil-cache",
6504
"tranquil-comms",
6505
"tranquil-crypto",
0
0
6506
"tranquil-infra",
6507
"tranquil-oauth",
6508
"tranquil-repo",
···
6375
"sqlx",
6376
"thiserror 2.0.17",
6377
"tokio",
6378
+
"tranquil-db-traits",
6379
"urlencoding",
6380
"uuid",
6381
]
···
6395
"sha2",
6396
"subtle",
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",
6432
]
6433
6434
[[package]]
···
6538
"tranquil-cache",
6539
"tranquil-comms",
6540
"tranquil-crypto",
6541
+
"tranquil-db",
6542
+
"tranquil-db-traits",
6543
"tranquil-infra",
6544
"tranquil-oauth",
6545
"tranquil-repo",
+5
-1
Cargo.toml
···
11
"crates/tranquil-auth",
12
"crates/tranquil-oauth",
13
"crates/tranquil-comms",
0
0
14
"crates/tranquil-pds",
15
]
16
···
30
tranquil-auth = { path = "crates/tranquil-auth" }
31
tranquil-oauth = { path = "crates/tranquil-oauth" }
32
tranquil-comms = { path = "crates/tranquil-comms" }
0
0
33
34
aes-gcm = "0.10"
35
backon = "1"
···
92
tracing = "0.1"
93
tracing-subscriber = "0.3"
94
urlencoding = "2.1"
95
-
uuid = { version = "1.19", features = ["v4", "v5", "v7", "fast-rng"] }
96
webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] }
97
webauthn-rs-proto = "0.5"
98
zip = { version = "7.0", default-features = false, features = ["deflate"] }
···
11
"crates/tranquil-auth",
12
"crates/tranquil-oauth",
13
"crates/tranquil-comms",
14
+
"crates/tranquil-db-traits",
15
+
"crates/tranquil-db",
16
"crates/tranquil-pds",
17
]
18
···
32
tranquil-auth = { path = "crates/tranquil-auth" }
33
tranquil-oauth = { path = "crates/tranquil-oauth" }
34
tranquil-comms = { path = "crates/tranquil-comms" }
35
+
tranquil-db-traits = { path = "crates/tranquil-db-traits" }
36
+
tranquil-db = { path = "crates/tranquil-db" }
37
38
aes-gcm = "0.10"
39
backon = "1"
···
96
tracing = "0.1"
97
tracing-subscriber = "0.3"
98
urlencoding = "2.1"
99
+
uuid = { version = "1.19", features = ["v4", "v5", "v7", "fast-rng", "serde"] }
100
webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] }
101
webauthn-rs-proto = "0.5"
102
zip = { version = "7.0", default-features = false, features = ["deflate"] }
+17
crates/tranquil-db-traits/Cargo.toml
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
}