mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-24 18:26:06 +00:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05246aec58 | ||
|
|
8d0e0c6810 | ||
|
|
1720de8650 | ||
|
|
a5c68a3506 | ||
|
|
9ea33def13 | ||
|
|
a9e3ec63e1 | ||
|
|
dbc3c23c95 | ||
|
|
531ca7c1c0 | ||
|
|
add1ff176b | ||
|
|
bdb200a30c | ||
|
|
e5faba3e73 | ||
|
|
5dc810ceef | ||
|
|
6abc0eaca2 | ||
|
|
696940f732 | ||
|
|
f777c288da | ||
|
|
9039648705 | ||
|
|
770a16b043 | ||
|
|
d37f38e496 | ||
|
|
282f08f207 | ||
|
|
f07bcb82f7 | ||
|
|
e38343ce09 | ||
|
|
e454e99b8a | ||
|
|
19b0ea19c6 | ||
|
|
318818ad51 | ||
|
|
7c55a5ceb9 | ||
|
|
9d31ee9ace | ||
|
|
e3efcb43ba | ||
|
|
eb46824220 |
@@ -13,9 +13,22 @@ fail-fast = false
|
||||
test-threads = "num-cpus"
|
||||
slow-timeout = { period = "30s", terminate-after = 4 }
|
||||
|
||||
[profile.sim-pr]
|
||||
retries = 0
|
||||
fail-fast = true
|
||||
test-threads = "num-cpus"
|
||||
slow-timeout = { period = "60s", terminate-after = 4 }
|
||||
|
||||
[profile.sim-nightly]
|
||||
retries = 0
|
||||
fail-fast = false
|
||||
test-threads = "num-cpus"
|
||||
slow-timeout = { period = "300s", terminate-after = 2 }
|
||||
|
||||
[test-groups]
|
||||
serial-env-tests = { max-threads = 1 }
|
||||
heavy-load-tests = { max-threads = 4 }
|
||||
io-heavy-sim = { max-threads = 2 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "test(/import_with_verification/) | test(/plc_migration/)"
|
||||
@@ -29,6 +42,10 @@ test-group = "serial-env-tests"
|
||||
filter = "binary(ripple_cluster)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "package(tranquil-signal)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(whole_story)"
|
||||
test-group = "heavy-load-tests"
|
||||
@@ -41,6 +58,20 @@ test-group = "heavy-load-tests"
|
||||
filter = "binary(repo_lifecycle)"
|
||||
test-group = "heavy-load-tests"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(sim_soak)"
|
||||
slow-timeout = { period = "300s", terminate-after = 10 }
|
||||
test-group = "io-heavy-sim"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(metastore_crash) | binary(sim_cross_store) | binary(sim_gc_concurrent) | binary(sim_reachability) | binary(sim_eventlog) | test(/sim_/)"
|
||||
slow-timeout = { period = "120s", terminate-after = 8 }
|
||||
test-group = "io-heavy-sim"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "test(/test_scale_/) | test(/full_backup_and_restore/)"
|
||||
slow-timeout = { period = "120s", terminate-after = 4 }
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "test(/import_with_verification/) | test(/plc_migration/)"
|
||||
test-group = "serial-env-tests"
|
||||
@@ -53,6 +84,10 @@ test-group = "serial-env-tests"
|
||||
filter = "binary(ripple_cluster)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "package(tranquil-signal)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(whole_story)"
|
||||
test-group = "heavy-load-tests"
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT seq, did, commit_cid\n FROM repo_seq\n WHERE event_type = 'commit'\n AND prev_cid IS NULL\n AND (blocks_cids IS NULL OR array_length(blocks_cids, 1) IS NULL OR array_length(blocks_cids, 1) = 0)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "commit_cid",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "033ed5638ef6e30f1648c1acdbc477c173798cb57eb0127c3a582a9633fa5012"
|
||||
}
|
||||
+18
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1 AND seq < $2\n ORDER BY seq ASC",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -50,26 +50,36 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "block_cids",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "block_data",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "blocks_cids",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 12,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 13,
|
||||
"name": "active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "rev",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -94,8 +104,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d8524ad3f5dc03eb09ed60396a78df5003f804c43ad253d6476523eacdebf811"
|
||||
"hash": "0349b2af4d21105029ecc2bf81ad7be9987ec17ac088730596849d7a45558e57"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET is_admin = $1 WHERE did = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1e63d287c619a14e5c07d80e8e54193d2964c8b5e6a855256cb80c4d0cd2c6ea"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT t.token, t.expires_at\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "237c2d912e89b7e0e5baa83503a22f158ea1614b5157f6c9e2aba6017fef6b26"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE repo_seq SET blocks_cids = $1 WHERE seq = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3b791fdb8e29043c980963d4d18e1e492c73c39818a8648a7af70555418fb5d1"
|
||||
}
|
||||
+18
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -50,33 +50,42 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "block_cids",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "block_data",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "blocks_cids",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 12,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 13,
|
||||
"name": "active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "rev",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
@@ -94,8 +103,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b26bf97a27783eb7fb524a92dda3e68ef8470a9751fcaefe5fd2d7909dead54b"
|
||||
"hash": "4702a94384ce938d3f5384bc2106a48500f8749134e6bfd1c619a9151f5d3772"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO backlinks (uri, path, link_to, repo_id)\n SELECT unnest($1::text[]), unnest($2::text[]), unnest($3::text[]), $4\n ON CONFLICT (uri, path) DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "47149c0577ad9e9b9b089820b0c93417769a4a37affe0e3972e324ec27ec532f"
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, rev)\n VALUES ($1, 'sync', $2, $3)\n RETURNING seq\n ",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, rev, block_cids, block_data)\n VALUES ($1, 'sync', $2, $3, $4, $5)\n RETURNING seq\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -13,12 +13,14 @@
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
"Text",
|
||||
"ByteaArray",
|
||||
"ByteaArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bce9edc597592d2e7d672061883639bcdeff80be85e2ecacb6c75d8c027e6710"
|
||||
"hash": "5996774d097a484f81353d49e4a99b07c30a003377c924add576bfb48ddde29b"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "code",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5cee16f49a727d66b5231a8d07d7f4bcb6a1136fbf3e3d249fd33600772ac80f"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT token, did, expires_at FROM account_deletion_requests WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "61f489b4fc42f5b0aaea287cde4415da6f5e96b3a0f36216bdc6dea924b09abd"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM repo_seq WHERE created_at < $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "746ca87e98f6eaa9c060770c0d1bca6f0d4da60df2dc2db088e57bdc914cf233"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO user_totp (did, secret_encrypted, encryption_version, verified, created_at)\n VALUES ($1, $2, 1, TRUE, NOW())\n ON CONFLICT (did) DO UPDATE SET secret_encrypted = $2, verified = TRUE",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7d3a9f0545943bc6a3a14fcd596aac5cc731c8177d74e504606d7e92c7d0c73f"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET two_factor_enabled = $1 WHERE did = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "84a1db51a98402323cb86bc19cd2b737f908222ea3426b8bf47d735aff5b6c75"
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM comms_queue WHERE user_id = $1 AND comms_type = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "comms_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"welcome",
|
||||
"email_verification",
|
||||
"password_reset",
|
||||
"email_update",
|
||||
"account_deletion",
|
||||
"admin_email",
|
||||
"plc_operation",
|
||||
"two_factor_code",
|
||||
"channel_verification",
|
||||
"passkey_recovery",
|
||||
"legacy_login_alert",
|
||||
"migration_verification",
|
||||
"channel_verified"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "89b0292d8d022fad8f9cda07b9a7870ca6a7ebe904b2d580956b0816b50bcdb7"
|
||||
}
|
||||
+19
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -50,32 +50,43 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "block_cids",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "block_data",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "blocks_cids",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 12,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 13,
|
||||
"name": "active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "rev",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
@@ -93,8 +104,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b8101757a50075d20147014e450cb7deb7e58f84310690c7bde61e1834dc5903"
|
||||
"hash": "89c8ce80d8c52b4668c34f63e48b51cacc7e8ff300958a192378f1d72de04f9a"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, prev_data_cid, rev)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n RETURNING seq\n ",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, block_cids, block_data, prev_data_cid, rev)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n RETURNING seq\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -17,7 +17,8 @@
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"ByteaArray",
|
||||
"ByteaArray",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
@@ -26,5 +27,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "40e848f48afb50f5e4820445aee874a482333e9090ef14ce44db3fd9721cfff0"
|
||||
"hash": "97dd3cfe72b74b4dac686c030a07f6eb56dabb2df72815f3248760b4e3f7950e"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as \"count!\"\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "990bf50e60fc5566639c2c12cd968d154d7b0c6863ad69141653135f98fbc998"
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n id, user_id,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: CommsType\",\n status as \"status: CommsStatus\",\n recipient, subject, body, metadata,\n attempts, max_attempts, last_error,\n created_at, updated_at, scheduled_for, processed_at\n FROM comms_queue\n WHERE user_id = $1 AND comms_type = $2\n ORDER BY created_at DESC\n LIMIT $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "channel: CommsChannel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "comms_channel",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"email",
|
||||
"discord",
|
||||
"telegram",
|
||||
"signal"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "comms_type: CommsType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "comms_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"welcome",
|
||||
"email_verification",
|
||||
"password_reset",
|
||||
"email_update",
|
||||
"account_deletion",
|
||||
"admin_email",
|
||||
"plc_operation",
|
||||
"two_factor_code",
|
||||
"channel_verification",
|
||||
"passkey_recovery",
|
||||
"legacy_login_alert",
|
||||
"migration_verification",
|
||||
"channel_verified"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "status: CommsStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "comms_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"processing",
|
||||
"sent",
|
||||
"failed"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "recipient",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "subject",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "body",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "attempts",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "max_attempts",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "last_error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "scheduled_for",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "processed_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "comms_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"welcome",
|
||||
"email_verification",
|
||||
"password_reset",
|
||||
"email_update",
|
||||
"account_deletion",
|
||||
"admin_email",
|
||||
"plc_operation",
|
||||
"two_factor_code",
|
||||
"channel_verification",
|
||||
"passkey_recovery",
|
||||
"legacy_login_alert",
|
||||
"migration_verification",
|
||||
"channel_verified"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9f3f2b36f11e9446915d3ca29ef81e4ada0c6a6d72764116dac4f99a4e09785e"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, rev)\n VALUES ($1, 'commit', $2, $3::TEXT, $4, $5, $6, $7)\n RETURNING seq\n ",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, block_cids, block_data, rev)\n VALUES ($1, 'commit', $2, $3::TEXT, $4, $5, $6, $7, $8)\n RETURNING seq\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -16,7 +16,8 @@
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"ByteaArray",
|
||||
"ByteaArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
@@ -24,5 +25,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "52df8ae809b990fa76e3ac45f37e352a9fc8f92f911a7b9c9be2fd9e8032b18c"
|
||||
"hash": "a325862f897484bb1a8863fd7a1bc3fd17ab20b6e30498f9900daf80609c413d"
|
||||
}
|
||||
+18
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1 AND seq < $2\n ORDER BY seq ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -50,26 +50,36 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "block_cids",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "block_data",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "blocks_cids",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 12,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 13,
|
||||
"name": "active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "rev",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -94,8 +104,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e7aa1080be9eb3a8ddf1f050c93dc8afd10478f41e22307014784b4ee3740b4a"
|
||||
"hash": "ad784aff9bb5c1f19f90a3b1c3b3694a3e1aa889b0fe3e55cfebb7af8c620256"
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as \"count!\" FROM comms_queue WHERE user_id = $1 AND comms_type = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "comms_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"welcome",
|
||||
"email_verification",
|
||||
"password_reset",
|
||||
"email_update",
|
||||
"account_deletion",
|
||||
"admin_email",
|
||||
"plc_operation",
|
||||
"two_factor_code",
|
||||
"channel_verification",
|
||||
"passkey_recovery",
|
||||
"legacy_login_alert",
|
||||
"migration_verification",
|
||||
"channel_verified"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "b364a2b202bab17c0cdc5f70d23b13841b4d9063d94cd0b09268c3dc41824fd2"
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT blocks_cids, commit_cid\n FROM repo_seq\n WHERE did = $1 AND rev > $2\n ORDER BY seq DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "blocks_cids",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "commit_cid",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c15d538fece4cd7db0056f9e0374a2290e26f5766881fd9dbf4f05788845aac0"
|
||||
}
|
||||
+18
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq = $1",
|
||||
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -50,26 +50,36 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "block_cids",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "block_data",
|
||||
"type_info": "ByteaArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "blocks_cids",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 12,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 13,
|
||||
"name": "active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "rev",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -93,8 +103,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0d32a592a97ad47c65aa37cf0d45417f2966fcbd688be7434626ae5f6971fa1f"
|
||||
"hash": "c47746c256fd8c9d386551f3bd270e9a96fecdb713642d07d60fb4aeb02c8426"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM backlinks WHERE uri = ANY($1::text[])",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cffe4c37fe949fbdc3d5cd83ccec5655aae248a0a69dc260d1da9cf1d9ed2c49"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4"
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, did, public_key_did_key, private_key_bytes, expires_at, used_at\n FROM reserved_signing_keys WHERE public_key_did_key = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "public_key_did_key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "private_key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "used_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f3b07f153284b6dd1f22c098af8628d85dcdf20dd6273443ff98d02b6f5ecbf1"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "password_reset_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "password_reset_code_expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
when:
|
||||
- event: ["push"]
|
||||
branches: main
|
||||
- event: []
|
||||
branch: []
|
||||
|
||||
engine: nixery
|
||||
|
||||
@@ -20,5 +20,5 @@ steps:
|
||||
|
||||
- name: Publish image
|
||||
command: |
|
||||
podman push --creds "$ATCR_USERNAME:$ATCR_PASSWORD" tranquil-pds:latest "atcr.io/tranquil-pds/tranquil:latest"
|
||||
podman push --creds "$ATCR_USERNAME:$ATCR_PASSWORD" "tranquil-pds:$TANGLED_COMMIT_SHA" "atcr.io/tranquil-pds/tranquil:$TANGLED_COMMIT_SHA"
|
||||
podman push --creds "$ATCR_USERNAME:$ATCR_PASSWORD" tranquil-pds:latest "atcr.io/tranquil.farm/tranquil-pds:latest"
|
||||
podman push --creds "$ATCR_USERNAME:$ATCR_PASSWORD" "tranquil-pds:$TANGLED_COMMIT_SHA" "atcr.io/tranquil.farm/tranquil-pds:$TANGLED_COMMIT_SHA"
|
||||
|
||||
Generated
+1227
-634
File diff suppressed because it is too large
Load Diff
+9
-2
@@ -22,10 +22,11 @@ members = [
|
||||
"crates/tranquil-api",
|
||||
"crates/tranquil-lexicon",
|
||||
"crates/tranquil-signal",
|
||||
"crates/tranquil-store",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.7"
|
||||
version = "0.5.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -50,7 +51,8 @@ tranquil-server = { path = "crates/tranquil-server" }
|
||||
tranquil-sync = { path = "crates/tranquil-sync" }
|
||||
tranquil-oauth-server = { path = "crates/tranquil-oauth-server" }
|
||||
tranquil-api = { path = "crates/tranquil-api" }
|
||||
tranquil-signal = { path = "crates/tranquil-signal" }
|
||||
tranquil-signal = { path = "crates/tranquil-signal", features = ["fjall-store"] }
|
||||
tranquil-store = { path = "crates/tranquil-store" }
|
||||
|
||||
presage = { git = "https://github.com/whisperfish/presage", rev = "fe3ed54c4844ae51c3a9fa49cf80a7816a31a425", default-features = false }
|
||||
|
||||
@@ -141,3 +143,8 @@ curve25519-dalek = { git = "https://github.com/signalapp/curve25519-dalek", tag
|
||||
lto = "fat"
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[profile.bench]
|
||||
debug = 1
|
||||
strip = false
|
||||
|
||||
+5
-13
@@ -4,7 +4,7 @@ COPY frontend/ ./
|
||||
RUN deno task build
|
||||
|
||||
FROM rust:1.92-alpine AS builder
|
||||
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang
|
||||
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang protoc
|
||||
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
|
||||
WORKDIR /app
|
||||
ARG SLIM="false"
|
||||
@@ -29,6 +29,8 @@ COPY crates/tranquil-pds ./crates/tranquil-pds
|
||||
COPY crates/tranquil-sync ./crates/tranquil-sync
|
||||
COPY crates/tranquil-api ./crates/tranquil-api
|
||||
COPY crates/tranquil-oauth-server ./crates/tranquil-oauth-server
|
||||
COPY crates/tranquil-store ./crates/tranquil-store
|
||||
COPY crates/tranquil-signal ./crates/tranquil-signal
|
||||
COPY crates/tranquil-server ./crates/tranquil-server
|
||||
COPY migrations ./crates/tranquil-pds/migrations
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
@@ -40,23 +42,13 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
fi && \
|
||||
cp target/release/tranquil-server /tmp/tranquil-pds
|
||||
|
||||
FROM alpine:3.23 AS signal-cli
|
||||
RUN apk add --no-cache curl tar
|
||||
ARG SIGNAL_CLI_VERSION=0.13.24
|
||||
RUN curl -fsSL "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-Linux-native.tar.gz" \
|
||||
| tar xz -C /usr/local/bin
|
||||
|
||||
FROM debian:trixie-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends msmtp ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
FROM alpine:3.23
|
||||
RUN apk add --no-cache msmtp ca-certificates \
|
||||
&& ln -sf /usr/bin/msmtp /usr/sbin/sendmail
|
||||
COPY --from=signal-cli /usr/local/bin/signal-cli /usr/local/bin/signal-cli
|
||||
VOLUME /var/lib/signal-cli
|
||||
COPY --from=builder /tmp/tranquil-pds /usr/local/bin/tranquil-pds
|
||||
COPY --from=frontend /app/dist /var/lib/tranquil-pds/frontend
|
||||
COPY migrations /app/migrations
|
||||
WORKDIR /app
|
||||
ENV SIGNAL_CLI_CONFIG=/var/lib/signal-cli
|
||||
ENV SERVER_HOST=0.0.0.0
|
||||
ENV SERVER_PORT=3000
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
tranquil-store: embedded storage engine for Tranquil PDS
|
||||
RFC draft, 2026-03-22
|
||||
By Lewis!
|
||||
|
||||
-- TLDR --
|
||||
|
||||
Add an embedded storage engine as an alternative to postgres (and leapfrog SQLite-per-actor)
|
||||
that treats Tranquil's 3 types of storage workloads as 3 separate problems:
|
||||
|
||||
- BlockStore: bitcask-esque append log for immutable CID-keyed blocks [4]
|
||||
- MetaStore: Fjall LSM keyspaces for mutable metadata [5]
|
||||
- EventLog: segmented append log for the firehose
|
||||
|
||||
Group commit across users, content dedup, sub-ms firehose delivery.
|
||||
We will use deterministic simulation testing [18][19].
|
||||
Postgres will of course stay as the existing alternative backend.
|
||||
|
||||
-- Intro --
|
||||
|
||||
The ref PDS hits structural limits around 300k accounts [2].
|
||||
SQLite-per-actor means no cross-user write batching.
|
||||
|
||||
tranquil-store is an embedded rust library. It lives in-process, no external deps.
|
||||
Postgres remains supported; we plan to enable a storage transition path
|
||||
such that users can seamlessly snapshot-n-switch between the backends.
|
||||
|
||||
The BlockStore is a bitcask-style append log [4] with a Fjall key index [5] that
|
||||
maps each CID to a (file, offset, length) tuple. We use key-value separation
|
||||
as per WiscKey [6]. Because blocks are immutable and keyed by CID, the value log
|
||||
never needs compaction. An LRU hot tier keeps frequently-accessed blocks in
|
||||
mem, and hint files allow fast index reconstruction on restart [4].
|
||||
|
||||
The main throughput enabler is group commit [7]. The ref PDS fsyncs once per user
|
||||
per mutation [1], but BlockStore batches all concurrent commits into a single
|
||||
write-and-sync cycle.
|
||||
|
||||
Content dedup occurs naturally: identical MST subtrees across users share
|
||||
one CID-keyed block instead of N copies [3].
|
||||
|
||||
MetaStore uses Fjall [5] keyspaces for all mutable data. We chose Fjall over
|
||||
redb and LMDB because both of those are single-writer [8][9]. Each keyspace
|
||||
compacts independently.
|
||||
|
||||
For cross-store atomicity we use an intent log. Each mutation writes a single
|
||||
intent record containing the BlockStore refcount updates, MetaStore changes,
|
||||
and the serialized EventLog payload, fsynced via the group commit. After fsync,
|
||||
the changes are applied to MetaStore and the event is appended to the EventLog,
|
||||
then the intent is marked committed. Recovery replays any incomplete intents,
|
||||
re-applying both metadata changes and event appends. This gives us crash-atomic
|
||||
mutations across all three stores without full MVCC, since mutations
|
||||
are already serialized per-user [3].
|
||||
|
||||
EventLog stores the firehose as segmented append-only files. Live subscribers
|
||||
receive events via tokio broadcast, and consumers that are catching up will
|
||||
read from mmap'ed segments [10]. Each event receives a monotonic u64 sequence number.
|
||||
Segment headers store the base sequence number; a per-segment index maps sequence
|
||||
ranges to byte offsets. This decouples consumer cursors from physical layout,
|
||||
allowing transparent addition of per-segment zstd compression per the loom-v2
|
||||
spec [11] without invalidating checkpoints. Retention is just deleting old
|
||||
segments! :P
|
||||
|
||||
GC uses refcounted key index entries. GC is epoch-gated by the group commit
|
||||
cycle: a block is only eligible for collection if its refcount reached zero in
|
||||
a prior completed commit cycle. This prevents races between concurrent dedup
|
||||
(which skips the block write but increments the refcount in the same batch) and
|
||||
collection. Blocks past the epoch gate are collected by rewriting any data
|
||||
files that fall below a liveness threshold.
|
||||
|
||||
For serialization we use postcard on disk and rkyv [12] for in-mem caches
|
||||
only. All data files carry a version tag.
|
||||
|
||||
Memory is divided into fixed slices from a configurable total budget: Fjall
|
||||
block cache, BlockStore hot tier, and CID index each receive a configured
|
||||
percentage. Actual usage per component is exposed as metrics. The EventLog's
|
||||
mmap pages live in the OS page cache and are excluded from the budget.
|
||||
|
||||
Backup acquires the group commit lock, which quiesces all writes at the next
|
||||
commit boundary. Under the lock, the system notes the EventLog position, the
|
||||
BlockStore file list, and takes a Fjall snapshot, then releases the lock.
|
||||
Sealed data files and segments are immutable and can be copied without
|
||||
co-ordination after the snapshot. The quiesce window is bounded by one commit
|
||||
cycle. Point-in-time recovery replays the EventLog against a prior snapshot.
|
||||
For continuous replication, a background process tails the EventLog and copies
|
||||
sealed files to remote storage.
|
||||
|
||||
-- Runtime --
|
||||
|
||||
The storage core runs on tokio. It is synchronous internally, accessed through
|
||||
dedicated handler threads that communicate via async channels [13]. Requests
|
||||
are dispatched by hashing the DID, which gives us per-user write serialization
|
||||
without locks. Global operations use round-robin. All disk IO goes through
|
||||
pread/pwrite directly [13].
|
||||
|
||||
We rejected io_uring for three reasons: it creates orphan kernel operations
|
||||
when futures are cancelled [22], it is blocked by default in both Docker [16]
|
||||
and Podman [17] seccomp profiles, and it accounts for 60% of Google's kernel
|
||||
vulnerability rewards [15].
|
||||
|
||||
We also rejected thread-per-core runtimes (glommio, etc.) because they are
|
||||
incompatible with the tokio ecosystem. DID-sharded handler threads give us
|
||||
the same shared-nothing property without a runtime split.
|
||||
|
||||
-- Testing --
|
||||
|
||||
We use deterministic simulation testing, following FoundationDB [18] and
|
||||
TigerBeetle's VOPR [19]. All IO sits behind a StorageIO trait, and tests use an
|
||||
in-memory implementation that injects faults: partial writes, bit flips, sync
|
||||
failures, and misdirected writes. A single seed controls the entire fault
|
||||
schedule, so any failure reproduces exactly [20][21].
|
||||
|
||||
-- Why these choices --
|
||||
|
||||
Bitcask for blocks:
|
||||
Key-val separation [6] using Bitcask [4] for immutable CID blocks:
|
||||
O(1) writes, O(1) reads, zero write amplification, & no compaction!
|
||||
|
||||
Fjall for metadata:
|
||||
Only pure-Rust embedded engine with concurrent writers [5].
|
||||
Otherwise we'd write our own.
|
||||
|
||||
Segmented log for events:
|
||||
Write once -> scan forward -> delete by age.
|
||||
Quite straightforward!
|
||||
|
||||
Postcard on disk:
|
||||
rkyv is apparently faster [12] but couples on-disk format to library version.
|
||||
|
||||
Tokio & handler threads:
|
||||
spawn_blocking & pread matches io_uring without security/compat costs [13][14][15][16].
|
||||
|
||||
Deterministic simulation:
|
||||
Catches bug classes conventional testing can't reach [18][19].
|
||||
StorageIO trait is needed anyway; but being harness-first is a one-time cost [20][21].
|
||||
|
||||
-- References --
|
||||
|
||||
[1] Bluesky PDS SQLite migration. github.com/bluesky-social/atproto/pull/1705
|
||||
[2] G. Orosz. Building Bluesky: a Distributed Social Network. Pragmatic Engineer, April 2024.
|
||||
newsletter.pragmaticengineer.com/p/bluesky
|
||||
K. Suder. Introduction to AT Protocol. August 2025. mackuba.eu/2025/08/20/introduction-to-atproto
|
||||
Bluesky PDS "Going to Production" guide. atproto.com/guides/going-to-production
|
||||
[3] AT Protocol repository spec. atproto.com/specs/repository
|
||||
[4] Bitcask: A Log-Structured Hash Table for Fast KV Data. Riak, 2010. riak.com/assets/bitcask-intro.pdf
|
||||
[5] Fjall: LSM-based embedded storage engine. github.com/fjall-rs/fjall
|
||||
[6] Lu et al. WiscKey: Separating Keys from Values in SSD-Conscious Storage. USENIX FAST 2016.
|
||||
usenix.org/conference/fast16/technical-sessions/presentation/lu
|
||||
[7] Phil Eaton. A Write-Ahead Log Is Not a Universal Part of Durability. July 2024.
|
||||
notes.eatonphil.com/2024-07-01-a-write-ahead-log-is-not-a-universal-part-of-durability.html
|
||||
[8] redb design document. github.com/cberner/redb/blob/master/docs/design.md
|
||||
[9] LMDB source repository. github.com/LMDB/lmdb
|
||||
[10] Crotty et al. Are You Sure You Want to Use MMAP in Your DBMS? CIDR 2022.
|
||||
cs.brown.edu/people/acrotty/pubs/p13-crotty.pdf
|
||||
[11] ybzeek. RFC: com.atproto.sync.getZstdStream (zstd-compressed relay streams).
|
||||
github.com/bluesky-social/atproto/discussions/4582
|
||||
[12] rkyv: zero-copy deserialization framework for Rust. rkyv.org
|
||||
[13] Tonbo. Exploring Better Async Rust Disk IO. tonbo.io/blog/exploring-better-async-rust-disk-io
|
||||
[14] Iroh. Async Rust Challenges in Iroh. iroh.computer/blog/async-rust-challenges-in-iroh
|
||||
[15] Google restricting io_uring. phoronix.com/news/Google-Restricting-IO_uring
|
||||
[16] Docker 4.42.0 and io_uring. forums.docker.com/t/4-42-0-and-io-uring/148620
|
||||
[17] Podman io_uring discussion. github.com/containers/podman/discussions/27772
|
||||
[18] FoundationDB simulation testing. apple.github.io/foundationdb/testing.html
|
||||
[19] TigerBeetle VOPR. tigerbeetle.com/blog/2023-07-06-simulation-testing-for-liveness
|
||||
[20] DST in Rust (S2). s2.dev/blog/dst
|
||||
[21] Phil Eaton. What's the big deal about Deterministic Simulation Testing? August 2024.
|
||||
notes.eatonphil.com/2024-08-20-deterministic-simulation-testing.html
|
||||
[22] Tonbo. Async Rust Is Not Safe with io_uring. tonbo.io/blog/async-rust-is-not-safe-with-io-uring
|
||||
|
||||
Thank you for reading! Let's do some great work together.
|
||||
|
||||
@@ -12,12 +12,15 @@ tranquil-db = { workspace = true }
|
||||
tranquil-db-traits = { workspace = true }
|
||||
tranquil-lexicon = { workspace = true, features = ["resolve"] }
|
||||
tranquil-scopes = { workspace = true }
|
||||
tranquil-signal = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
backon = { workspace = true }
|
||||
base32 = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
image = { workspace = true }
|
||||
qrcodegen = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
|
||||
@@ -34,13 +34,13 @@ pub struct GetPreferencesOutput {
|
||||
}
|
||||
pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Permissive>) -> Response {
|
||||
let has_full_access = auth.permissions().has_full_access();
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await {
|
||||
let user_id: uuid::Uuid = match state.repos.user.get_id_by_did(&auth.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return ApiError::InternalError(Some("User not found".into())).into_response();
|
||||
}
|
||||
};
|
||||
let prefs = match state.infra_repo.get_account_preferences(user_id).await {
|
||||
let prefs = match state.repos.infra.get_account_preferences(user_id).await {
|
||||
Ok(rows) => rows,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Failed to fetch preferences".into()))
|
||||
@@ -93,7 +93,7 @@ pub async fn put_preferences(
|
||||
Json(input): Json<PutPreferencesInput>,
|
||||
) -> Response {
|
||||
let has_full_access = auth.permissions().has_full_access();
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await {
|
||||
let user_id: uuid::Uuid = match state.repos.user.get_id_by_did(&auth.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return ApiError::InternalError(Some("User not found".into())).into_response();
|
||||
@@ -188,7 +188,8 @@ pub async fn put_preferences(
|
||||
.collect();
|
||||
|
||||
if state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.replace_namespace_preferences(user_id, APP_BSKY_NAMESPACE, prefs_to_save)
|
||||
.await
|
||||
.is_err()
|
||||
|
||||
@@ -19,7 +19,8 @@ pub async fn delete_account(
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
let did = &input.did;
|
||||
let (user_id, handle) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_and_handle_by_did(did)
|
||||
.await
|
||||
.log_db_err("in delete_account")?
|
||||
@@ -27,7 +28,8 @@ pub async fn delete_account(
|
||||
.map(|row| (row.id, row.handle))?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.admin_delete_account_complete(user_id, did)
|
||||
.await
|
||||
.log_db_err("deleting account")?;
|
||||
|
||||
@@ -31,7 +31,8 @@ pub async fn send_email(
|
||||
return Err(ApiError::InvalidRequest("content is required".into()));
|
||||
}
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_by_did(&input.recipient_did)
|
||||
.await
|
||||
.log_db_err("in send_email")?
|
||||
@@ -45,7 +46,8 @@ pub async fn send_email(
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("Message from {}", hostname));
|
||||
let result = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
|
||||
@@ -69,7 +69,8 @@ pub async fn get_account_info(
|
||||
Query(params): Query<GetAccountInfoParams>,
|
||||
) -> Result<Json<AccountInfo>, ApiError> {
|
||||
let account = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_admin_account_info_by_did(¶ms.did)
|
||||
.await
|
||||
.log_db_err("in get_account_info")?
|
||||
@@ -98,7 +99,8 @@ pub async fn get_account_info(
|
||||
|
||||
async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> {
|
||||
let code = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_code_used_by_user(user_id)
|
||||
.await
|
||||
.ok()??;
|
||||
@@ -111,7 +113,8 @@ async fn get_invites_for_user(
|
||||
user_id: uuid::Uuid,
|
||||
) -> Option<Vec<InviteCodeInfo>> {
|
||||
let invite_codes = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invites_created_by_user(user_id)
|
||||
.await
|
||||
.ok()?;
|
||||
@@ -123,7 +126,8 @@ async fn get_invites_for_user(
|
||||
let code_strings: Vec<String> = invite_codes.iter().map(|ic| ic.code.clone()).collect();
|
||||
|
||||
let uses = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_code_uses_batch(&code_strings)
|
||||
.await
|
||||
.ok()?;
|
||||
@@ -154,10 +158,11 @@ async fn get_invites_for_user(
|
||||
}
|
||||
|
||||
async fn get_invite_code_info(state: &AppState, code: &str) -> Option<InviteCodeInfo> {
|
||||
let info = state.infra_repo.get_invite_code_info(code).await.ok()??;
|
||||
let info = state.repos.infra.get_invite_code_info(code).await.ok()??;
|
||||
|
||||
let uses = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_code_uses(code)
|
||||
.await
|
||||
.ok()
|
||||
@@ -197,7 +202,8 @@ pub async fn get_account_infos(
|
||||
|
||||
let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect();
|
||||
let accounts = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_admin_account_infos_by_dids(&dids_typed)
|
||||
.await
|
||||
.log_db_err("fetching account infos")?;
|
||||
@@ -205,7 +211,8 @@ pub async fn get_account_infos(
|
||||
let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect();
|
||||
|
||||
let all_invite_codes = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_codes_by_users(&user_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -217,7 +224,8 @@ pub async fn get_account_infos(
|
||||
|
||||
let all_invite_uses = if !all_codes.is_empty() {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_code_uses_batch(&all_codes)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -226,7 +234,8 @@ pub async fn get_account_infos(
|
||||
};
|
||||
|
||||
let invited_by_map: HashMap<uuid::Uuid, String> = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_code_uses_by_users(&user_ids)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use info::{
|
||||
};
|
||||
pub use search::{SearchAccountsOutput, SearchAccountsParams, search_accounts};
|
||||
pub use update::{
|
||||
UpdateAccountEmailInput, UpdateAccountHandleInput, UpdateAccountPasswordInput,
|
||||
update_account_email, update_account_handle, update_account_password,
|
||||
SetAdminStatusInput, UpdateAccountEmailInput, UpdateAccountHandleInput,
|
||||
UpdateAccountPasswordInput, set_admin_status, update_account_email, update_account_handle,
|
||||
update_account_password,
|
||||
};
|
||||
|
||||
@@ -51,15 +51,14 @@ pub async fn search_accounts(
|
||||
Query(params): Query<SearchAccountsParams>,
|
||||
) -> Result<Json<SearchAccountsOutput>, ApiError> {
|
||||
let limit = params.limit.clamp(1, 100);
|
||||
let email_filter = params.email.as_deref().map(|e| format!("%{}%", e));
|
||||
let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
|
||||
let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok());
|
||||
let rows = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.search_accounts(
|
||||
cursor_did.as_ref(),
|
||||
email_filter.as_deref(),
|
||||
handle_filter.as_deref(),
|
||||
params.email.as_deref(),
|
||||
params.handle.as_deref(),
|
||||
limit + 1,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::{Json, extract::State};
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, warn};
|
||||
use tracing::{error, info, warn};
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
@@ -30,7 +30,8 @@ pub async fn update_account_email(
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.admin_update_email(&account_did, email)
|
||||
.await
|
||||
{
|
||||
@@ -71,9 +72,10 @@ pub async fn update_account_handle(
|
||||
} else {
|
||||
input_handle.to_string()
|
||||
};
|
||||
let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten();
|
||||
let old_handle = state.repos.user.get_handle_by_did(did).await.ok().flatten();
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
@@ -81,14 +83,16 @@ pub async fn update_account_handle(
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
let handle_for_check: Handle = handle.parse().map_err(|_| ApiError::InvalidHandle(None))?;
|
||||
if let Ok(true) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.check_handle_exists(&handle_for_check, user_id)
|
||||
.await
|
||||
{
|
||||
return Err(ApiError::HandleTaken);
|
||||
}
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.admin_update_handle(did, &handle_for_check)
|
||||
.await
|
||||
{
|
||||
@@ -146,13 +150,11 @@ pub async fn update_account_password(
|
||||
if password.is_empty() {
|
||||
return Err(ApiError::InvalidRequest("password is required".into()));
|
||||
}
|
||||
let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let password_hash = crate::common::hash_or_internal_error(password)?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.admin_update_password(did, &password_hash)
|
||||
.await
|
||||
{
|
||||
@@ -164,3 +166,34 @@ pub async fn update_account_password(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetAdminStatusInput {
|
||||
pub did: Did,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
pub async fn set_admin_status(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Admin>,
|
||||
Json(input): Json<SetAdminStatusInput>,
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
info!(
|
||||
actor = %auth.did,
|
||||
target = %input.did,
|
||||
admin = input.admin,
|
||||
"admin status change"
|
||||
);
|
||||
|
||||
state
|
||||
.repos
|
||||
.user
|
||||
.set_admin_status(&input.did, input.admin)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error setting admin status: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
Ok(Json(EmptyResponse {}))
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ pub async fn get_server_config(
|
||||
];
|
||||
|
||||
let rows = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_server_configs(keys)
|
||||
.await
|
||||
.log_db_err("fetching server config")?;
|
||||
@@ -86,7 +87,8 @@ pub async fn update_server_config(
|
||||
));
|
||||
}
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_server_config("server_name", trimmed)
|
||||
.await
|
||||
.log_db_err("upserting server_name")?;
|
||||
@@ -95,13 +97,15 @@ pub async fn update_server_config(
|
||||
if let Some(ref color) = req.primary_color {
|
||||
if color.is_empty() {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.delete_server_config("primary_color")
|
||||
.await
|
||||
.log_db_err("deleting primary_color")?;
|
||||
} else if is_valid_hex_color(color) {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_server_config("primary_color", color)
|
||||
.await
|
||||
.log_db_err("upserting primary_color")?;
|
||||
@@ -115,13 +119,15 @@ pub async fn update_server_config(
|
||||
if let Some(ref color) = req.primary_color_dark {
|
||||
if color.is_empty() {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.delete_server_config("primary_color_dark")
|
||||
.await
|
||||
.log_db_err("deleting primary_color_dark")?;
|
||||
} else if is_valid_hex_color(color) {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_server_config("primary_color_dark", color)
|
||||
.await
|
||||
.log_db_err("upserting primary_color_dark")?;
|
||||
@@ -135,13 +141,15 @@ pub async fn update_server_config(
|
||||
if let Some(ref color) = req.secondary_color {
|
||||
if color.is_empty() {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.delete_server_config("secondary_color")
|
||||
.await
|
||||
.log_db_err("deleting secondary_color")?;
|
||||
} else if is_valid_hex_color(color) {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_server_config("secondary_color", color)
|
||||
.await
|
||||
.log_db_err("upserting secondary_color")?;
|
||||
@@ -155,13 +163,15 @@ pub async fn update_server_config(
|
||||
if let Some(ref color) = req.secondary_color_dark {
|
||||
if color.is_empty() {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.delete_server_config("secondary_color_dark")
|
||||
.await
|
||||
.log_db_err("deleting secondary_color_dark")?;
|
||||
} else if is_valid_hex_color(color) {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_server_config("secondary_color_dark", color)
|
||||
.await
|
||||
.log_db_err("upserting secondary_color_dark")?;
|
||||
@@ -174,7 +184,8 @@ pub async fn update_server_config(
|
||||
|
||||
if let Some(ref logo_cid) = req.logo_cid {
|
||||
let old_logo_cid = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_server_config("logo_cid")
|
||||
.await
|
||||
.ok()
|
||||
@@ -189,13 +200,16 @@ pub async fn update_server_config(
|
||||
if let Some(old_cid_str) = should_delete_old {
|
||||
match CidLink::new(old_cid_str) {
|
||||
Ok(old_cid) => {
|
||||
if let Ok(Some(storage_key)) =
|
||||
state.infra_repo.get_blob_storage_key_by_cid(&old_cid).await
|
||||
if let Ok(Some(storage_key)) = state
|
||||
.repos
|
||||
.infra
|
||||
.get_blob_storage_key_by_cid(&old_cid)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = state.blob_store.delete(&storage_key).await {
|
||||
error!("Failed to delete old logo blob from storage: {:?}", e);
|
||||
}
|
||||
if let Err(e) = state.infra_repo.delete_blob_by_cid(&old_cid).await {
|
||||
if let Err(e) = state.repos.infra.delete_blob_by_cid(&old_cid).await {
|
||||
error!("Failed to delete old logo blob record: {:?}", e);
|
||||
}
|
||||
}
|
||||
@@ -211,13 +225,15 @@ pub async fn update_server_config(
|
||||
|
||||
if logo_cid.is_empty() {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.delete_server_config("logo_cid")
|
||||
.await
|
||||
.log_db_err("deleting logo_cid")?;
|
||||
} else {
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_server_config("logo_cid", logo_cid)
|
||||
.await
|
||||
.log_db_err("upserting logo_cid")?;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn disable_invite_codes(
|
||||
Json(input): Json<DisableInviteCodesInput>,
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
if let Some(codes) = &input.codes
|
||||
&& let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await
|
||||
&& let Err(e) = state.repos.infra.disable_invite_codes_by_code(codes).await
|
||||
{
|
||||
error!("DB error disabling invite codes: {:?}", e);
|
||||
}
|
||||
@@ -32,7 +32,8 @@ pub async fn disable_invite_codes(
|
||||
let accounts_typed: Vec<tranquil_types::Did> =
|
||||
accounts.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
if let Err(e) = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.disable_invite_codes_by_account(&accounts_typed)
|
||||
.await
|
||||
{
|
||||
@@ -87,7 +88,8 @@ pub async fn get_invite_codes(
|
||||
};
|
||||
|
||||
let codes_rows = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.list_invite_codes(params.cursor.as_deref(), limit, sort_order)
|
||||
.await
|
||||
.log_db_err("fetching invite codes")?;
|
||||
@@ -96,7 +98,8 @@ pub async fn get_invite_codes(
|
||||
let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect();
|
||||
|
||||
let creator_dids: std::collections::HashMap<uuid::Uuid, tranquil_types::Did> = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_user_dids_by_ids(&user_ids)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -108,7 +111,8 @@ pub async fn get_invite_codes(
|
||||
} else {
|
||||
common::group_invite_uses_by_code(
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_code_uses_batch(&code_strings)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
@@ -168,7 +172,8 @@ pub async fn disable_account_invites(
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_invites_disabled(&account_did, true)
|
||||
.await
|
||||
{
|
||||
@@ -200,7 +205,8 @@ pub async fn enable_account_invites(
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_invites_disabled(&account_did, false)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -2,15 +2,17 @@ pub mod account;
|
||||
pub mod config;
|
||||
pub mod invite;
|
||||
pub mod server_stats;
|
||||
pub mod signal;
|
||||
pub mod status;
|
||||
|
||||
pub use account::{
|
||||
delete_account, get_account_info, get_account_infos, search_accounts, send_email,
|
||||
update_account_email, update_account_handle, update_account_password,
|
||||
set_admin_status, update_account_email, update_account_handle, update_account_password,
|
||||
};
|
||||
pub use config::{get_server_config, update_server_config};
|
||||
pub use invite::{
|
||||
disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes,
|
||||
};
|
||||
pub use server_stats::get_server_stats;
|
||||
pub use signal::{get_signal_status, link_signal_device, unlink_signal_device};
|
||||
pub use status::{get_subject_status, update_subject_status};
|
||||
|
||||
@@ -17,10 +17,10 @@ pub async fn get_server_stats(
|
||||
State(state): State<AppState>,
|
||||
_auth: Auth<Admin>,
|
||||
) -> Result<Json<ServerStatsOutput>, ApiError> {
|
||||
let user_count = state.user_repo.count_users().await.unwrap_or(0);
|
||||
let repo_count = state.repo_repo.count_repos().await.unwrap_or(0);
|
||||
let record_count = state.repo_repo.count_all_records().await.unwrap_or(0);
|
||||
let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0);
|
||||
let user_count = state.repos.user.count_users().await.unwrap_or(0);
|
||||
let repo_count = state.repos.repo.count_repos().await.unwrap_or(0);
|
||||
let record_count = state.repos.repo.count_all_records().await.unwrap_or(0);
|
||||
let blob_storage_bytes = state.repos.blob.sum_blob_storage().await.unwrap_or(0);
|
||||
|
||||
Ok(Json(ServerStatsOutput {
|
||||
user_count,
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
use axum::{Json, extract::State};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use image::{ImageBuffer, Luma};
|
||||
use serde::Serialize;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignalStatusOutput {
|
||||
pub enabled: bool,
|
||||
pub linked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignalLinkOutput {
|
||||
pub qr_base64: String,
|
||||
}
|
||||
|
||||
pub async fn get_signal_status(
|
||||
State(state): State<AppState>,
|
||||
_auth: Auth<Admin>,
|
||||
) -> Result<Json<SignalStatusOutput>, ApiError> {
|
||||
let enabled = tranquil_config::get().signal.enabled;
|
||||
let linked = match &state.signal_sender {
|
||||
Some(slot) => slot.is_linked().await,
|
||||
None => false,
|
||||
};
|
||||
|
||||
Ok(Json(SignalStatusOutput { enabled, linked }))
|
||||
}
|
||||
|
||||
pub async fn link_signal_device(
|
||||
State(state): State<AppState>,
|
||||
_auth: Auth<Admin>,
|
||||
) -> Result<Json<SignalLinkOutput>, ApiError> {
|
||||
let slot = state
|
||||
.signal_sender
|
||||
.as_ref()
|
||||
.ok_or_else(|| ApiError::InvalidRequest("Signal is not enabled".into()))?;
|
||||
|
||||
if slot.is_linked().await {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Signal device already linked".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let (generation, link_cancel) = slot.begin_link().await;
|
||||
|
||||
let device_name = tranquil_signal::DeviceName::new("tranquil-pds".to_string())
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("invalid device name: {e}"))))?;
|
||||
|
||||
let signal_store = state
|
||||
.signal_store_provider
|
||||
.as_ref()
|
||||
.ok_or_else(|| ApiError::InternalError(Some("Signal store not configured".into())))?;
|
||||
|
||||
let link_result = signal_store
|
||||
.link_signal_device(
|
||||
device_name,
|
||||
state.shutdown.clone(),
|
||||
link_cancel,
|
||||
slot.linking_flag(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Signal linking failed: {e}"))))?;
|
||||
|
||||
let qr_base64 = url_to_qr_png_base64(link_result.url.as_str())
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("QR generation failed: {e}"))))?;
|
||||
|
||||
let slot_for_task = slot.clone();
|
||||
let shutdown = state.shutdown.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.cancelled() => {
|
||||
tracing::info!("Signal linking aborted due to server shutdown");
|
||||
return;
|
||||
}
|
||||
r = link_result.completion => r,
|
||||
};
|
||||
match result {
|
||||
Ok(Ok(client)) => {
|
||||
if slot_for_task.complete_link(generation, client).await {
|
||||
tracing::info!("Signal device linked successfully");
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Signal link completed but generation mismatch or already linked; discarding"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(error = %e, "Signal device linking failed");
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("Signal linking task dropped without completing");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Json(SignalLinkOutput { qr_base64 }))
|
||||
}
|
||||
|
||||
pub async fn unlink_signal_device(
|
||||
State(state): State<AppState>,
|
||||
_auth: Auth<Admin>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let slot = state
|
||||
.signal_sender
|
||||
.as_ref()
|
||||
.ok_or_else(|| ApiError::InvalidRequest("Signal is not enabled".into()))?;
|
||||
|
||||
let signal_store = state
|
||||
.signal_store_provider
|
||||
.as_ref()
|
||||
.ok_or_else(|| ApiError::InternalError(Some("Signal store not configured".into())))?;
|
||||
|
||||
signal_store
|
||||
.clear_signal_data()
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Failed to clear signal data: {e}"))))?;
|
||||
|
||||
slot.unlink().await;
|
||||
|
||||
Ok(Json(serde_json::json!({})))
|
||||
}
|
||||
|
||||
const QR_MODULE_SCALE: u32 = 8;
|
||||
const QR_QUIET_ZONE_MODULES: u32 = 4;
|
||||
|
||||
fn url_to_qr_png_base64(url: &str) -> Result<String, String> {
|
||||
let qr = qrcodegen::QrCode::encode_text(url, qrcodegen::QrCodeEcc::Medium)
|
||||
.map_err(|e| format!("QR encode failed: {e:?}"))?;
|
||||
let size = u32::try_from(qr.size()).map_err(|_| "QR size is negative".to_string())?;
|
||||
let img_size = size
|
||||
.checked_add(
|
||||
QR_QUIET_ZONE_MODULES
|
||||
.checked_mul(2)
|
||||
.ok_or("border overflow")?,
|
||||
)
|
||||
.ok_or("image size overflow")?
|
||||
.checked_mul(QR_MODULE_SCALE)
|
||||
.ok_or("scaled size overflow")?;
|
||||
|
||||
let img: ImageBuffer<Luma<u8>, Vec<u8>> = ImageBuffer::from_fn(img_size, img_size, |x, y| {
|
||||
let module_x = x / QR_MODULE_SCALE;
|
||||
let module_y = y / QR_MODULE_SCALE;
|
||||
match (
|
||||
module_x.checked_sub(QR_QUIET_ZONE_MODULES),
|
||||
module_y.checked_sub(QR_QUIET_ZONE_MODULES),
|
||||
) {
|
||||
(Some(mx), Some(my)) if mx < size && my < size => {
|
||||
if qr.get_module(mx as i32, my as i32) {
|
||||
Luma([0u8])
|
||||
} else {
|
||||
Luma([255u8])
|
||||
}
|
||||
}
|
||||
_ => Luma([255u8]),
|
||||
}
|
||||
});
|
||||
|
||||
let mut png_bytes = Vec::new();
|
||||
let mut cursor = std::io::Cursor::new(&mut png_bytes);
|
||||
img.write_to(&mut cursor, image::ImageFormat::Png)
|
||||
.map_err(|e| format!("PNG encode failed: {e}"))?;
|
||||
|
||||
Ok(STANDARD.encode(&png_bytes))
|
||||
}
|
||||
@@ -45,7 +45,7 @@ pub async fn get_subject_status(
|
||||
let did: Did = did_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
match state.user_repo.get_status_by_did(&did).await {
|
||||
match state.repos.user.get_status_by_did(&did).await {
|
||||
Ok(Some(status)) => {
|
||||
let deactivated = status.deactivated_at.map(|_| StatusAttr {
|
||||
applied: true,
|
||||
@@ -77,7 +77,7 @@ pub async fn get_subject_status(
|
||||
let cid: CidLink = uri_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?;
|
||||
match state.repo_repo.get_record_by_cid(&cid).await {
|
||||
match state.repos.repo.get_record_by_cid(&cid).await {
|
||||
Ok(Some(record)) => {
|
||||
let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr {
|
||||
applied: true,
|
||||
@@ -109,7 +109,7 @@ pub async fn get_subject_status(
|
||||
let did = params.did.as_ref().ok_or_else(|| {
|
||||
ApiError::InvalidRequest("Must provide a did to request blob state".into())
|
||||
})?;
|
||||
match state.blob_repo.get_blob_with_takedown(&blob_cid).await {
|
||||
match state.repos.blob.get_blob_with_takedown(&blob_cid).await {
|
||||
Ok(Some(blob)) => {
|
||||
let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr {
|
||||
applied: true,
|
||||
@@ -172,7 +172,8 @@ pub async fn update_subject_status(
|
||||
None
|
||||
};
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_user_takedown(&did, takedown_ref)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -182,9 +183,9 @@ pub async fn update_subject_status(
|
||||
}
|
||||
if let Some(deactivated) = &input.deactivated {
|
||||
let result = if deactivated.applied {
|
||||
state.user_repo.deactivate_account(&did, None).await
|
||||
state.repos.user.deactivate_account(&did, None).await
|
||||
} else {
|
||||
state.user_repo.activate_account(&did).await
|
||||
state.repos.user.activate_account(&did).await
|
||||
};
|
||||
result.map_err(|e| {
|
||||
error!(
|
||||
@@ -194,11 +195,22 @@ pub async fn update_subject_status(
|
||||
ApiError::InternalError(Some("Failed to update deactivation status".into()))
|
||||
})?;
|
||||
}
|
||||
let takedown_update = input.takedown.as_ref().map(|t| t.applied);
|
||||
let takedown_ref = input.takedown.as_ref().and_then(|t| t.r#ref.as_deref());
|
||||
let deactivated_update = input.deactivated.as_ref().map(|d| d.applied);
|
||||
if (takedown_update.is_some() || deactivated_update.is_some())
|
||||
&& let Err(e) = state
|
||||
.repos
|
||||
.repo
|
||||
.update_repo_status(&did, takedown_update, takedown_ref, deactivated_update)
|
||||
.await
|
||||
{
|
||||
warn!("failed to sync status to repo backend: {e:?}");
|
||||
}
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let status = if takedown.applied {
|
||||
tranquil_db_traits::AccountStatus::Takendown
|
||||
} else {
|
||||
tranquil_db_traits::AccountStatus::Active
|
||||
let status = match takedown.applied {
|
||||
true => tranquil_db_traits::AccountStatus::Takendown,
|
||||
false => tranquil_db_traits::AccountStatus::Active,
|
||||
};
|
||||
if let Err(e) =
|
||||
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
|
||||
@@ -207,10 +219,9 @@ pub async fn update_subject_status(
|
||||
}
|
||||
}
|
||||
if let Some(deactivated) = &input.deactivated {
|
||||
let status = if deactivated.applied {
|
||||
tranquil_db_traits::AccountStatus::Deactivated
|
||||
} else {
|
||||
tranquil_db_traits::AccountStatus::Active
|
||||
let status = match deactivated.applied {
|
||||
true => tranquil_db_traits::AccountStatus::Deactivated,
|
||||
false => tranquil_db_traits::AccountStatus::Active,
|
||||
};
|
||||
if let Err(e) =
|
||||
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
|
||||
@@ -218,7 +229,7 @@ pub async fn update_subject_status(
|
||||
warn!("Failed to sequence account event for deactivation: {}", e);
|
||||
}
|
||||
}
|
||||
if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await {
|
||||
if let Ok(Some(handle)) = state.repos.user.get_handle_by_did(&did).await {
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
|
||||
@@ -249,7 +260,8 @@ pub async fn update_subject_status(
|
||||
None
|
||||
};
|
||||
state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.set_record_takedown(&cid, takedown_ref)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -282,7 +294,8 @@ pub async fn update_subject_status(
|
||||
None
|
||||
};
|
||||
state
|
||||
.blob_repo
|
||||
.repos
|
||||
.blob
|
||||
.update_blob_takedown(&cid, takedown_ref)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
||||
@@ -68,8 +68,8 @@ async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option
|
||||
let http_uri = "/";
|
||||
|
||||
let auth_user = match validate_token_with_dpop(
|
||||
state.user_repo.as_ref(),
|
||||
state.oauth_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.oauth.as_ref(),
|
||||
&extracted.token,
|
||||
extracted.scheme,
|
||||
dpop_proof,
|
||||
@@ -89,7 +89,7 @@ async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option
|
||||
}
|
||||
};
|
||||
|
||||
match state.user_repo.get_by_did(&auth_user.did).await {
|
||||
match state.repos.user.get_by_did(&auth_user.did).await {
|
||||
Ok(Some(user)) => {
|
||||
tracing::debug!(created_at = ?user.created_at, "age assurance: got user");
|
||||
Some(user.created_at.to_rfc3339())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use bcrypt::DEFAULT_COST;
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use tracing::error;
|
||||
@@ -245,6 +245,20 @@ pub fn hash_or_internal_error(value: &str) -> Result<String, ApiError> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn hash_password_async(password: &str) -> Result<String, ApiError> {
|
||||
let password = password.to_string();
|
||||
tokio::task::spawn_blocking(move || hash(password, DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_token_hash(
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
stored_hash: &str,
|
||||
|
||||
@@ -24,7 +24,8 @@ pub async fn list_controllers(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<ControllersOutput<Vec<tranquil_db_traits::ControllerInfo>>>, ApiError> {
|
||||
let controllers = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.get_delegations_for_account(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -37,8 +38,9 @@ pub async fn list_controllers(
|
||||
async move {
|
||||
if c.handle.is_none() {
|
||||
c.handle = did_resolver
|
||||
.resolve_did_document(c.did.as_str())
|
||||
.fetch_did_document(c.did.as_str())
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc))
|
||||
.map(Into::into);
|
||||
}
|
||||
@@ -64,7 +66,7 @@ pub async fn add_controller(
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did)
|
||||
.await
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
.map_err(|_| ApiError::ControllerNotFound)?;
|
||||
|
||||
if !resolved.is_local
|
||||
&& let Some(ref pds_url) = resolved.pds_url
|
||||
@@ -99,7 +101,8 @@ pub async fn add_controller(
|
||||
|
||||
if resolved.is_local
|
||||
&& state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.is_delegated_account(&input.controller_did)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
@@ -110,7 +113,8 @@ pub async fn add_controller(
|
||||
}
|
||||
|
||||
match state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.create_delegation(
|
||||
can_add.did(),
|
||||
&input.controller_did,
|
||||
@@ -121,7 +125,8 @@ pub async fn add_controller(
|
||||
{
|
||||
Ok(_) => {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
can_add.did(),
|
||||
can_add.did(),
|
||||
@@ -158,13 +163,15 @@ pub async fn remove_controller(
|
||||
Json(input): Json<RemoveControllerInput>,
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.revoke_delegation(&auth.did, &input.controller_did, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let revoked_app_passwords = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.delete_app_passwords_by_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
@@ -172,13 +179,15 @@ pub async fn remove_controller(
|
||||
.unwrap_or(0usize);
|
||||
|
||||
let revoked_oauth_tokens = state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.revoke_tokens_for_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&auth.did,
|
||||
&auth.did,
|
||||
@@ -217,13 +226,15 @@ pub async fn update_controller_scopes(
|
||||
Json(input): Json<UpdateControllerScopesInput>,
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&auth.did,
|
||||
&auth.did,
|
||||
@@ -254,7 +265,8 @@ pub async fn list_controlled_accounts(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<AccountsOutput<Vec<tranquil_db_traits::DelegatedAccountInfo>>>, ApiError> {
|
||||
let accounts = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.get_accounts_controlled_by(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -286,7 +298,8 @@ pub async fn get_audit_log(
|
||||
let offset = params.offset.max(0);
|
||||
|
||||
let entries = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.get_audit_log_for_account(&auth.did, limit, offset)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -295,7 +308,8 @@ pub async fn get_audit_log(
|
||||
})?;
|
||||
|
||||
let total = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.count_audit_log_entries(&auth.did)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -349,7 +363,7 @@ pub async fn create_delegated_account(
|
||||
}
|
||||
|
||||
let validated_invite_code = if let Some(ref code) = input.invite_code {
|
||||
match state.infra_repo.validate_invite_code(code).await {
|
||||
match state.repos.infra.validate_invite_code(code).await {
|
||||
Ok(validated) => Some(validated),
|
||||
Err(_) => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
@@ -387,7 +401,8 @@ pub async fn create_delegated_account(
|
||||
};
|
||||
|
||||
let user_id = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.create_delegated_account(&create_input)
|
||||
.await
|
||||
{
|
||||
@@ -406,7 +421,8 @@ pub async fn create_delegated_account(
|
||||
|
||||
if let Some(validated) = validated_invite_code
|
||||
&& let Err(e) = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.record_invite_code_use(&validated, user_id)
|
||||
.await
|
||||
{
|
||||
@@ -423,7 +439,8 @@ pub async fn create_delegated_account(
|
||||
.await;
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&did,
|
||||
&auth.did,
|
||||
@@ -461,7 +478,7 @@ pub async fn resolve_controller(
|
||||
} else {
|
||||
let local_handle: Option<Handle> = identifier.parse().ok();
|
||||
let local_user = match local_handle {
|
||||
Some(ref h) => state.user_repo.get_by_handle(h).await.ok().flatten(),
|
||||
Some(ref h) => state.repos.user.get_by_handle(h).await.ok().flatten(),
|
||||
None => None,
|
||||
};
|
||||
match local_user {
|
||||
@@ -476,7 +493,7 @@ pub async fn resolve_controller(
|
||||
|
||||
let resolved = tranquil_pds::delegation::resolve_identity(&state, &did)
|
||||
.await
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
.map_err(|_| ApiError::ControllerNotFound)?;
|
||||
|
||||
Ok(Json(resolved))
|
||||
}
|
||||
|
||||
@@ -169,7 +169,8 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response {
|
||||
);
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.store_discord_user_id(&discord_username, &discord_user_id, handle.as_deref())
|
||||
.await
|
||||
{
|
||||
@@ -180,8 +181,8 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response {
|
||||
"Verified Discord user and stored user ID"
|
||||
);
|
||||
if let Err(e) = comms_repo::enqueue_channel_verified(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
tranquil_db_traits::CommsChannel::Discord,
|
||||
&discord_user_id,
|
||||
|
||||
@@ -6,7 +6,6 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::{debug, error, info};
|
||||
@@ -16,7 +15,6 @@ use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle, PlainPassword};
|
||||
use tranquil_pds::validation::validate_password;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateAccountInput {
|
||||
@@ -68,14 +66,16 @@ async fn try_reactivate_migration(
|
||||
new_email: email.clone(),
|
||||
};
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.reactivate_migration_account(&reactivate_input)
|
||||
.await
|
||||
{
|
||||
Ok(reactivated) => {
|
||||
info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration");
|
||||
let secret_key_bytes = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_key_by_id(reactivated.user_id)
|
||||
.await
|
||||
{
|
||||
@@ -130,7 +130,7 @@ async fn try_reactivate_migration(
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
if let Err(e) = state.session_repo.create_session(&session_data).await {
|
||||
if let Err(e) = state.repos.session.create_session(&session_data).await {
|
||||
error!("Error creating session: {:?}", e);
|
||||
return Some(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
@@ -154,7 +154,12 @@ async fn try_reactivate_migration(
|
||||
Json(CreateAccountOutput {
|
||||
handle: handle.to_string().into(),
|
||||
did: did_typed.clone(),
|
||||
did_doc: state.did_resolver.resolve_did_document(did).await,
|
||||
did_doc: state
|
||||
.did_resolver
|
||||
.fetch_did_document(did)
|
||||
.await
|
||||
.ok()
|
||||
.map(|f| (*f).clone()),
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
verification_required,
|
||||
@@ -395,7 +400,8 @@ pub async fn create_account(
|
||||
Err(_) => return ApiError::InvalidHandle(None).into_response(),
|
||||
};
|
||||
let handle_available = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.check_handle_available_for_new_account(&handle_typed)
|
||||
.await
|
||||
{
|
||||
@@ -410,7 +416,7 @@ pub async fn create_account(
|
||||
}
|
||||
|
||||
let is_bootstrap = state.bootstrap_invite_code.is_some()
|
||||
&& state.user_repo.count_users().await.unwrap_or(1) == 0;
|
||||
&& state.repos.user.count_users().await.unwrap_or(1) == 0;
|
||||
|
||||
if is_bootstrap {
|
||||
match input.invite_code.as_deref() {
|
||||
@@ -431,7 +437,7 @@ pub async fn create_account(
|
||||
if let Some(code) = &input.invite_code
|
||||
&& !code.trim().is_empty()
|
||||
{
|
||||
let valid = match state.user_repo.check_and_consume_invite_code(code).await {
|
||||
let valid = match state.repos.user.check_and_consume_invite_code(code).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Error checking invite code: {:?}", e);
|
||||
@@ -448,19 +454,10 @@ pub async fn create_account(
|
||||
return ApiError::InvalidRequest(e.to_string()).into_response();
|
||||
}
|
||||
|
||||
let password_clone = input.password.clone();
|
||||
let password_hash =
|
||||
match tokio::task::spawn_blocking(move || hash(&password_clone, DEFAULT_COST)).await {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(e)) => {
|
||||
error!("Error hashing password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let password_hash = match crate::common::hash_password_async(&input.password).await {
|
||||
Ok(h) => h,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
|
||||
let deactivated_at: Option<chrono::DateTime<chrono::Utc>> = if is_migration || is_did_web_byod {
|
||||
Some(chrono::Utc::now())
|
||||
@@ -527,7 +524,12 @@ pub async fn create_account(
|
||||
birthdate_pref,
|
||||
};
|
||||
|
||||
let create_result = match state.user_repo.create_password_account(&create_input).await {
|
||||
let create_result = match state
|
||||
.repos
|
||||
.user
|
||||
.create_password_account(&create_input)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return ApiError::HandleNotAvailable(None).into_response();
|
||||
@@ -590,7 +592,7 @@ pub async fn create_account(
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
|
||||
let did_doc = state.did_resolver.resolve_did_document(&did).await;
|
||||
let did_doc = state.did_resolver.fetch_did_document(&did).await.ok();
|
||||
|
||||
if is_migration {
|
||||
info!(
|
||||
@@ -604,7 +606,7 @@ pub async fn create_account(
|
||||
Json(CreateAccountOutput {
|
||||
handle: handle.clone().into(),
|
||||
did: did_for_commit,
|
||||
did_doc,
|
||||
did_doc: did_doc.map(|f| (*f).clone()),
|
||||
access_jwt: session.access_jwt,
|
||||
refresh_jwt: session.refresh_jwt,
|
||||
verification_required: !is_migration,
|
||||
|
||||
@@ -54,7 +54,7 @@ pub async fn resolve_handle(
|
||||
return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
|
||||
}
|
||||
};
|
||||
let user = state.user_repo.get_by_handle(&handle).await;
|
||||
let user = state.repos.user.get_by_handle(&handle).await;
|
||||
match user {
|
||||
Ok(Some(row)) => {
|
||||
let _ = state
|
||||
@@ -165,7 +165,8 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
let user = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_for_did_doc_build(&expected_did_typed)
|
||||
.await
|
||||
{
|
||||
@@ -182,7 +183,8 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
|
||||
let did = expected_did;
|
||||
|
||||
let overrides = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_did_web_overrides(user_id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -218,7 +220,8 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
}
|
||||
};
|
||||
let user = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_did_web_info_by_handle(¤t_handle_typed)
|
||||
.await
|
||||
{
|
||||
@@ -246,7 +249,8 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
}
|
||||
|
||||
let overrides = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_did_web_overrides(user_id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -295,7 +299,7 @@ async fn build_override_or_key_verification_methods(
|
||||
.collect());
|
||||
}
|
||||
|
||||
let key_info = match state.user_repo.get_user_key_by_id(user_id).await {
|
||||
let key_info = match state.repos.user.get_user_key_by_id(user_id).await {
|
||||
Ok(Some(k)) => k,
|
||||
_ => return Err(ApiError::InternalError(None).into_response()),
|
||||
};
|
||||
@@ -468,7 +472,8 @@ pub async fn get_recommended_did_credentials(
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Json<GetRecommendedDidCredentialsOutput>, ApiError> {
|
||||
let handle = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_handle_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching handle for DID credentials")?
|
||||
@@ -539,7 +544,8 @@ pub async fn update_handle(
|
||||
)
|
||||
.await?;
|
||||
let user_row = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_and_handle_by_did(&did)
|
||||
.await
|
||||
.log_db_err("fetching user for handle update")?
|
||||
@@ -661,7 +667,8 @@ pub async fn update_handle(
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?;
|
||||
let handle_exists = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.check_handle_exists(&handle_typed, user_id)
|
||||
.await
|
||||
.log_db_err("checking handle existence")?;
|
||||
@@ -669,7 +676,8 @@ pub async fn update_handle(
|
||||
return Err(ApiError::HandleTaken);
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_handle(user_id, &handle_typed)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -706,7 +714,7 @@ pub async fn update_plc_handle(
|
||||
if !did.as_str().starts_with("did:plc:") {
|
||||
return Ok(());
|
||||
}
|
||||
let user_row = match state.user_repo.get_user_with_key_by_did(did).await? {
|
||||
let user_row = match state.repos.user.get_user_with_key_by_did(did).await? {
|
||||
Some(r) => r,
|
||||
None => return Ok(()),
|
||||
};
|
||||
@@ -733,7 +741,7 @@ pub async fn well_known_atproto_did(State(state): State<AppState>, headers: Head
|
||||
Ok(h) => h,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid handle format").into_response(),
|
||||
};
|
||||
let user = state.user_repo.get_by_handle(&handle).await;
|
||||
let user = state.repos.user.get_by_handle(&handle).await;
|
||||
match user {
|
||||
Ok(Some(row)) => row.did.to_string().into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(),
|
||||
|
||||
@@ -20,25 +20,27 @@ pub async fn request_plc_operation_signature(
|
||||
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
|
||||
)?;
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching user id")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await;
|
||||
let _ = state.repos.infra.delete_plc_tokens_for_user(user_id).await;
|
||||
let plc_token = generate_plc_token();
|
||||
let expires_at = Utc::now() + Duration::minutes(10);
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.insert_plc_token(user_id, &plc_token, expires_at)
|
||||
.await
|
||||
.log_db_err("creating PLC token")?;
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_plc_operation(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
&plc_token,
|
||||
hostname,
|
||||
|
||||
@@ -55,25 +55,28 @@ pub async fn sign_plc_operation(
|
||||
})?;
|
||||
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.log_db_err("fetching user id")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let token_expiry = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_plc_token_expiry(user_id, token)
|
||||
.await
|
||||
.log_db_err("fetching PLC token expiry")?
|
||||
.ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?;
|
||||
|
||||
if Utc::now() > token_expiry {
|
||||
let _ = state.infra_repo.delete_plc_token(user_id, token).await;
|
||||
let _ = state.repos.infra.delete_plc_token(user_id, token).await;
|
||||
return Err(ApiError::ExpiredToken(Some("Token has expired".into())));
|
||||
}
|
||||
let key_row = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_key_by_id(user_id)
|
||||
.await
|
||||
.log_db_err("fetching user key")?
|
||||
@@ -136,7 +139,7 @@ pub async fn sign_plc_operation(
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let _ = state.infra_repo.delete_plc_token(user_id, token).await;
|
||||
let _ = state.repos.infra.delete_plc_token(user_id, token).await;
|
||||
info!("Signed PLC operation for user {}", did);
|
||||
Ok(Json(SignPlcOperationOutput {
|
||||
operation: signed_op,
|
||||
|
||||
@@ -38,14 +38,16 @@ pub async fn submit_plc_operation(
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let public_url = format!("https://{}", hostname);
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_and_handle_by_did(did)
|
||||
.await
|
||||
.log_db_err("fetching user")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let key_row = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_key_by_id(user.id)
|
||||
.await
|
||||
.log_db_err("fetching user key")?
|
||||
@@ -128,12 +130,13 @@ pub async fn submit_plc_operation(
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
match state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.insert_identity_event(did, Some(&user.handle))
|
||||
.await
|
||||
{
|
||||
Ok(seq) => {
|
||||
if let Err(e) = state.repo_repo.notify_update(seq).await {
|
||||
if let Err(e) = state.repos.repo.notify_update(seq).await {
|
||||
warn!("Failed to notify identity event: {:?}", e);
|
||||
}
|
||||
}
|
||||
@@ -153,7 +156,7 @@ pub async fn submit_plc_operation(
|
||||
.cache
|
||||
.delete(&tranquil_pds::cache_keys::plc_data_key(did))
|
||||
.await;
|
||||
if state.did_resolver.refresh_did(did).await.is_none() {
|
||||
if state.did_resolver.refresh_did(did).await.is_err() {
|
||||
warn!(did = %did, "Failed to refresh DID cache after PLC update");
|
||||
}
|
||||
info!(did = %did, "PLC operation submitted successfully");
|
||||
|
||||
@@ -136,7 +136,8 @@ pub async fn resolve_signing_key(
|
||||
match signing_key_did {
|
||||
Some(key_did) => {
|
||||
let key = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_reserved_signing_key(key_did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -295,7 +296,8 @@ pub async fn create_and_store_session(
|
||||
app_password_name: None,
|
||||
};
|
||||
state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.create_session(&session_data)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -320,8 +322,8 @@ pub async fn enqueue_signup_verification(
|
||||
let formatted = tranquil_pds::auth::verification_token::format_token_for_display(&token);
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
channel,
|
||||
recipient,
|
||||
@@ -346,8 +348,8 @@ pub async fn enqueue_migration_verification(
|
||||
let formatted = tranquil_pds::auth::verification_token::format_token_for_display(&token);
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
channel,
|
||||
recipient,
|
||||
|
||||
@@ -330,6 +330,13 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
get(admin::get_invite_codes),
|
||||
)
|
||||
.route("/_admin.getServerStats", get(admin::get_server_stats))
|
||||
.route("/_admin.setAdminStatus", post(admin::set_admin_status))
|
||||
.route("/_admin.getSignalStatus", get(admin::get_signal_status))
|
||||
.route("/_admin.linkSignalDevice", post(admin::link_signal_device))
|
||||
.route(
|
||||
"/_admin.unlinkSignalDevice",
|
||||
post(admin::unlink_signal_device),
|
||||
)
|
||||
.route("/_server.getConfig", get(admin::get_server_config))
|
||||
.route(
|
||||
"/_admin.updateServerConfig",
|
||||
|
||||
@@ -107,7 +107,7 @@ async fn proxy_to_report_service(
|
||||
|
||||
let key_bytes = match &auth_user.key_bytes {
|
||||
Some(kb) => kb.clone(),
|
||||
None => match state.user_repo.get_with_key_by_did(&auth_user.did).await {
|
||||
None => match state.repos.user.get_with_key_by_did(&auth_user.did).await {
|
||||
Ok(Some(user_with_key)) => {
|
||||
match tranquil_pds::config::decrypt_key(
|
||||
&user_with_key.key_bytes,
|
||||
@@ -138,7 +138,7 @@ async fn proxy_to_report_service(
|
||||
let service_token = match tranquil_pds::auth::create_service_token(
|
||||
&auth_user.did,
|
||||
service_did,
|
||||
"com.atproto.moderation.createReport",
|
||||
Some("com.atproto.moderation.createReport"),
|
||||
&key_bytes,
|
||||
) {
|
||||
Ok(t) => t,
|
||||
@@ -226,7 +226,8 @@ async fn create_report_locally(
|
||||
let subject_json = json!(input.subject);
|
||||
|
||||
if let Err(e) = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.insert_report(
|
||||
report_id,
|
||||
input.reason_type.as_str(),
|
||||
|
||||
@@ -26,7 +26,8 @@ pub async fn get_notification_prefs(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<NotificationPrefsOutput>, ApiError> {
|
||||
let prefs = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_notification_prefs(&auth.did)
|
||||
.await
|
||||
.log_db_err("get notification prefs")?
|
||||
@@ -65,14 +66,16 @@ pub async fn get_notification_history(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<GetNotificationHistoryOutput>, ApiError> {
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("get user id by did")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let rows = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_notification_history(user_id, 50)
|
||||
.await
|
||||
.log_db_err("get notification history")?;
|
||||
@@ -146,7 +149,7 @@ pub async fn request_channel_verification(
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let handle_str = handle.unwrap_or("user");
|
||||
tranquil_pds::comms::comms_repo::enqueue_email_update(
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
identifier,
|
||||
handle_str,
|
||||
@@ -165,7 +168,8 @@ pub async fn request_channel_verification(
|
||||
hostname, encoded_token, encoded_identifier
|
||||
);
|
||||
let prefs = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_comms_prefs(user_id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -185,7 +189,8 @@ pub async fn request_channel_verification(
|
||||
);
|
||||
let recipient = match channel {
|
||||
CommsChannel::Telegram => state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_telegram_chat_id(user_id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -195,7 +200,8 @@ pub async fn request_channel_verification(
|
||||
_ => identifier.to_string(),
|
||||
};
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.enqueue_comms(
|
||||
Some(user_id),
|
||||
channel,
|
||||
@@ -238,17 +244,20 @@ async fn process_messaging_channel_update(
|
||||
}
|
||||
match channel {
|
||||
CommsChannel::Discord => state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.clear_discord(user_id)
|
||||
.await
|
||||
.log_db_err("clear discord")?,
|
||||
CommsChannel::Telegram => state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.clear_telegram(user_id)
|
||||
.await
|
||||
.log_db_err("clear telegram")?,
|
||||
CommsChannel::Signal => state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.clear_signal(user_id)
|
||||
.await
|
||||
.log_db_err("clear signal")?,
|
||||
@@ -281,17 +290,20 @@ async fn process_messaging_channel_update(
|
||||
|
||||
match channel {
|
||||
CommsChannel::Discord => state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_unverified_discord(user_id, &clean)
|
||||
.await
|
||||
.log_db_err("set unverified discord")?,
|
||||
CommsChannel::Telegram => state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_unverified_telegram(user_id, &clean)
|
||||
.await
|
||||
.log_db_err("set unverified telegram")?,
|
||||
CommsChannel::Signal => state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_unverified_signal(user_id, &clean)
|
||||
.await
|
||||
.log_db_err("set unverified signal")?,
|
||||
@@ -313,7 +325,8 @@ pub async fn update_notification_prefs(
|
||||
Json(input): Json<UpdateNotificationPrefsInput>,
|
||||
) -> Result<Json<UpdateNotificationPrefsOutput>, ApiError> {
|
||||
let user_row = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_handle_email_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("get user by did")?
|
||||
@@ -324,7 +337,8 @@ pub async fn update_notification_prefs(
|
||||
let current_email = user_row.email;
|
||||
|
||||
let current_prefs = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_notification_prefs(&auth.did)
|
||||
.await
|
||||
.log_db_err("get notification prefs for update")?
|
||||
@@ -347,7 +361,8 @@ pub async fn update_notification_prefs(
|
||||
|
||||
if input.preferred_channel.is_some() {
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_preferred_comms_channel(&auth.did, effective_channel)
|
||||
.await
|
||||
.log_db_err("update preferred channel")?;
|
||||
|
||||
@@ -66,7 +66,8 @@ pub async fn upload_blob(
|
||||
};
|
||||
|
||||
if state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.is_account_migrated(&did)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
@@ -78,7 +79,8 @@ pub async fn upload_blob(
|
||||
get_header_str(&headers, http::header::CONTENT_TYPE).unwrap_or("application/octet-stream");
|
||||
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(&did)
|
||||
.await
|
||||
.log_db_err("fetching user id for blob upload")?
|
||||
@@ -143,7 +145,8 @@ pub async fn upload_blob(
|
||||
);
|
||||
|
||||
match state
|
||||
.blob_repo
|
||||
.repos
|
||||
.blob
|
||||
.insert_blob(
|
||||
&cid_link,
|
||||
&mime_type,
|
||||
@@ -163,7 +166,7 @@ pub async fn upload_blob(
|
||||
|
||||
if let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await {
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
if let Err(db_err) = state.blob_repo.delete_blob_by_cid(&cid_link).await {
|
||||
if let Err(db_err) = state.repos.blob.delete_blob_by_cid(&cid_link).await {
|
||||
error!(
|
||||
"Failed to clean up orphaned blob record after copy failure: {:?}",
|
||||
db_err
|
||||
@@ -177,7 +180,8 @@ pub async fn upload_blob(
|
||||
|
||||
if let Some(ref controller) = controller_did
|
||||
&& let Err(e) = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&did,
|
||||
controller,
|
||||
@@ -236,7 +240,8 @@ pub async fn list_missing_blobs(
|
||||
) -> Result<Json<ListMissingBlobsOutput>, ApiError> {
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_by_did(did)
|
||||
.await
|
||||
.log_db_err("fetching user")?
|
||||
@@ -245,7 +250,8 @@ pub async fn list_missing_blobs(
|
||||
let limit = params.limit.unwrap_or(500).clamp(1, 1000);
|
||||
let cursor = params.cursor.as_deref();
|
||||
let missing = state
|
||||
.blob_repo
|
||||
.repos
|
||||
.blob
|
||||
.list_missing_blobs(user.id, cursor, limit + 1)
|
||||
.await
|
||||
.log_db_err("fetching missing blobs")?;
|
||||
|
||||
@@ -34,7 +34,8 @@ pub async fn import_repo(
|
||||
}
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_by_did(did)
|
||||
.await
|
||||
.log_db_err("fetching user")?
|
||||
@@ -44,7 +45,8 @@ pub async fn import_repo(
|
||||
}
|
||||
let user_id = user.id;
|
||||
let expected_root_cid = state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.get_repo_root_cid_by_user_id(user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -193,8 +195,18 @@ pub async fn import_repo(
|
||||
}
|
||||
let max_blocks = tranquil_config::get().import.max_blocks as usize;
|
||||
let _write_lock = state.repo_write_locks.lock(user_id).await;
|
||||
|
||||
state
|
||||
.block_store
|
||||
.put_many(blocks.clone())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to store import blocks: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
match apply_import(
|
||||
&state.repo_repo,
|
||||
&state.repos.repo,
|
||||
user_id,
|
||||
root,
|
||||
blocks.clone(),
|
||||
@@ -232,7 +244,8 @@ pub async fn import_repo(
|
||||
blob_refs.into_iter().unzip();
|
||||
|
||||
match state
|
||||
.blob_repo
|
||||
.repos
|
||||
.blob
|
||||
.insert_record_blobs(user_id, &record_uris, &blob_cids)
|
||||
.await
|
||||
{
|
||||
@@ -248,7 +261,8 @@ pub async fn import_repo(
|
||||
}
|
||||
}
|
||||
let key_row = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_with_key_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -289,7 +303,8 @@ pub async fn import_repo(
|
||||
})?;
|
||||
let new_root_cid_link = CidLink::from(&new_root_cid);
|
||||
state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.update_repo_root(user_id, &new_root_cid_link, &new_rev_str)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -299,7 +314,8 @@ pub async fn import_repo(
|
||||
let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect();
|
||||
all_block_cids.push(new_root_cid.to_bytes());
|
||||
state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.insert_user_blocks(user_id, &all_block_cids, &new_rev_str)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -312,7 +328,8 @@ pub async fn import_repo(
|
||||
new_root_str, new_rev_str
|
||||
);
|
||||
if !is_migration
|
||||
&& let Err(e) = sequence_import_event(&state, did, &new_root_cid_link).await
|
||||
&& let Err(e) =
|
||||
sequence_import_event(&state, did, &new_root_cid_link, &commit_bytes).await
|
||||
{
|
||||
warn!("Failed to sequence import event: {:?}", e);
|
||||
}
|
||||
@@ -322,7 +339,8 @@ pub async fn import_repo(
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
});
|
||||
if let Err(e) = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.insert_account_preference_if_not_exists(
|
||||
user_id,
|
||||
"app.bsky.actor.defs#personalDetailsPref",
|
||||
@@ -378,20 +396,28 @@ async fn sequence_import_event(
|
||||
state: &AppState,
|
||||
did: &Did,
|
||||
commit_cid: &CidLink,
|
||||
) -> Result<(), tranquil_db::DbError> {
|
||||
let data = tranquil_db::CommitEventData {
|
||||
commit_bytes: &[u8],
|
||||
) -> Result<(), tranquil_db_traits::DbError> {
|
||||
let commit_cid_parsed = commit_cid
|
||||
.to_cid()
|
||||
.expect("CidLink invariant: validated at construction");
|
||||
let inline_commit = tranquil_db_traits::EventBlockInline {
|
||||
cid_bytes: commit_cid_parsed.to_bytes(),
|
||||
data: commit_bytes.to_vec(),
|
||||
};
|
||||
let data = tranquil_db_traits::CommitEventData {
|
||||
did: did.clone(),
|
||||
event_type: tranquil_db::RepoEventType::Commit,
|
||||
event_type: tranquil_db_traits::RepoEventType::Commit,
|
||||
commit_cid: Some(commit_cid.clone()),
|
||||
prev_cid: None,
|
||||
ops: Some(serde_json::json!([])),
|
||||
blobs: Some(vec![]),
|
||||
blocks_cids: Some(vec![]),
|
||||
blocks: Some(vec![inline_commit]),
|
||||
prev_data_cid: None,
|
||||
rev: None,
|
||||
};
|
||||
|
||||
let seq = state.repo_repo.insert_commit_event(&data).await?;
|
||||
state.repo_repo.notify_update(seq).await?;
|
||||
let seq = state.repos.repo.insert_commit_event(&data).await?;
|
||||
state.repos.repo.notify_update(seq).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,12 +18,13 @@ pub async fn describe_repo(
|
||||
State(state): State<AppState>,
|
||||
Query(input): Query<DescribeRepoInput>,
|
||||
) -> Response {
|
||||
let resolved = match common::resolve_repo(state.user_repo.as_ref(), &input.repo).await {
|
||||
let resolved = match common::resolve_repo(state.repos.user.as_ref(), &input.repo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
let collections = state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.list_collections(resolved.user_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -6,14 +6,16 @@ use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
use tranquil_db_traits::Backlink;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{
|
||||
Active, Auth, WriteOpKind, require_not_migrated, require_verified_or_delegated,
|
||||
verify_batch_write_scopes,
|
||||
};
|
||||
use tranquil_pds::repo::tracking::TrackingBlockStore;
|
||||
use tranquil_pds::repo::TrackingBlockStore;
|
||||
use tranquil_pds::repo_ops::{
|
||||
FinalizeParams, RecordOp, begin_repo_write, extract_blob_cids, finalize_repo_write,
|
||||
FinalizeParams, RecordOp, begin_repo_write, extract_backlinks, extract_blob_cids,
|
||||
finalize_repo_write,
|
||||
};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{AtIdentifier, AtUri, Did, Nsid, Rkey};
|
||||
@@ -27,6 +29,8 @@ struct WriteAccumulator {
|
||||
ops: Vec<RecordOp>,
|
||||
modified_keys: Vec<String>,
|
||||
all_blob_cids: Vec<String>,
|
||||
backlinks_to_add: Vec<Backlink>,
|
||||
backlinks_to_remove: Vec<AtUri>,
|
||||
}
|
||||
|
||||
async fn process_single_write(
|
||||
@@ -42,6 +46,8 @@ async fn process_single_write(
|
||||
mut ops,
|
||||
mut modified_keys,
|
||||
mut all_blob_cids,
|
||||
mut backlinks_to_add,
|
||||
mut backlinks_to_remove,
|
||||
} = acc;
|
||||
|
||||
match write {
|
||||
@@ -79,6 +85,7 @@ async fn process_single_write(
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to add to MST".into())))?;
|
||||
let uri = AtUri::from_parts(did, collection, &rkey);
|
||||
backlinks_to_add.extend(extract_backlinks(&uri, value));
|
||||
results.push(WriteResult::CreateResult {
|
||||
uri,
|
||||
cid: record_cid.to_string(),
|
||||
@@ -87,7 +94,7 @@ async fn process_single_write(
|
||||
ops.push(RecordOp::Create {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
cid: record_cid,
|
||||
cid: tranquil_pds::cid_types::RecordCid::from(record_cid),
|
||||
});
|
||||
Ok(WriteAccumulator {
|
||||
mst: new_mst,
|
||||
@@ -95,6 +102,8 @@ async fn process_single_write(
|
||||
ops,
|
||||
modified_keys,
|
||||
all_blob_cids,
|
||||
backlinks_to_add,
|
||||
backlinks_to_remove,
|
||||
})
|
||||
}
|
||||
WriteOp::Update {
|
||||
@@ -125,12 +134,22 @@ async fn process_single_write(
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to store record".into())))?;
|
||||
let key = format!("{}/{}", collection, rkey);
|
||||
modified_keys.push(key.clone());
|
||||
let prev_record_cid = mst.get(&key).await.ok().flatten();
|
||||
let prev_record_cid = mst
|
||||
.get(&key)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::InternalError(Some(format!("Failed to read prev record: {}", e)))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidRequest("Update target record does not exist".into())
|
||||
})?;
|
||||
let new_mst = mst
|
||||
.update(&key, record_cid)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to update MST".into())))?;
|
||||
let uri = AtUri::from_parts(did, collection, rkey);
|
||||
backlinks_to_remove.push(uri.clone());
|
||||
backlinks_to_add.extend(extract_backlinks(&uri, value));
|
||||
results.push(WriteResult::UpdateResult {
|
||||
uri,
|
||||
cid: record_cid.to_string(),
|
||||
@@ -139,8 +158,8 @@ async fn process_single_write(
|
||||
ops.push(RecordOp::Update {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
cid: record_cid,
|
||||
prev: prev_record_cid,
|
||||
cid: tranquil_pds::cid_types::RecordCid::from(record_cid),
|
||||
prev: tranquil_pds::cid_types::RecordCid::from(prev_record_cid),
|
||||
});
|
||||
Ok(WriteAccumulator {
|
||||
mst: new_mst,
|
||||
@@ -148,21 +167,32 @@ async fn process_single_write(
|
||||
ops,
|
||||
modified_keys,
|
||||
all_blob_cids,
|
||||
backlinks_to_add,
|
||||
backlinks_to_remove,
|
||||
})
|
||||
}
|
||||
WriteOp::Delete { collection, rkey } => {
|
||||
let key = format!("{}/{}", collection, rkey);
|
||||
modified_keys.push(key.clone());
|
||||
let prev_record_cid = mst.get(&key).await.ok().flatten();
|
||||
let prev_record_cid = mst
|
||||
.get(&key)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::InternalError(Some(format!("Failed to read prev record: {}", e)))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidRequest("Delete target record does not exist".into())
|
||||
})?;
|
||||
let new_mst = mst
|
||||
.delete(&key)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to delete from MST".into())))?;
|
||||
backlinks_to_remove.push(AtUri::from_parts(did, collection, rkey));
|
||||
results.push(WriteResult::DeleteResult {});
|
||||
ops.push(RecordOp::Delete {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
prev: prev_record_cid,
|
||||
prev: tranquil_pds::cid_types::RecordCid::from(prev_record_cid),
|
||||
});
|
||||
Ok(WriteAccumulator {
|
||||
mst: new_mst,
|
||||
@@ -170,6 +200,8 @@ async fn process_single_write(
|
||||
ops,
|
||||
modified_keys,
|
||||
all_blob_cids,
|
||||
backlinks_to_add,
|
||||
backlinks_to_remove,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -189,6 +221,8 @@ async fn process_writes(
|
||||
ops: Vec::new(),
|
||||
modified_keys: Vec::new(),
|
||||
all_blob_cids: Vec::new(),
|
||||
backlinks_to_add: Vec::new(),
|
||||
backlinks_to_remove: Vec::new(),
|
||||
};
|
||||
stream::iter(writes.iter().map(Ok::<_, ApiError>))
|
||||
.try_fold(initial_acc, |acc, write| async move {
|
||||
@@ -304,7 +338,8 @@ pub async fn apply_writes(
|
||||
require_verified_or_delegated(&state, batch_proof.user()).await?;
|
||||
|
||||
let user_id: uuid::Uuid = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(&did)
|
||||
.await
|
||||
.log_db_err("fetching user for batch write")?
|
||||
@@ -318,6 +353,8 @@ pub async fn apply_writes(
|
||||
ops,
|
||||
modified_keys,
|
||||
all_blob_cids,
|
||||
backlinks_to_add,
|
||||
backlinks_to_remove,
|
||||
} = process_writes(
|
||||
&input.writes,
|
||||
mst,
|
||||
@@ -372,6 +409,8 @@ pub async fn apply_writes(
|
||||
ops,
|
||||
modified_keys: &modified_keys,
|
||||
blob_cids: &all_blob_cids,
|
||||
backlinks_to_add,
|
||||
backlinks_to_remove,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use crate::repo::record::write::{CommitInfo, prepare_repo_write};
|
||||
use axum::{Json, extract::State};
|
||||
use cid::Cid;
|
||||
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Active, Auth, VerifyScope};
|
||||
use tranquil_pds::repo::tracking::TrackingBlockStore;
|
||||
use tranquil_pds::repo_ops::{
|
||||
CommitError, FinalizeParams, RecordOp, begin_repo_write, finalize_repo_write,
|
||||
};
|
||||
use tranquil_pds::cid_types::RecordCid;
|
||||
use tranquil_pds::repo_ops::{FinalizeParams, RecordOp, begin_repo_write, finalize_repo_write};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{AtIdentifier, AtUri, Did, Nsid, Rkey};
|
||||
use tranquil_pds::types::{AtIdentifier, AtUri, Nsid, Rkey};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteRecordInput {
|
||||
@@ -59,23 +55,27 @@ pub async fn delete_record(
|
||||
}
|
||||
}
|
||||
|
||||
let prev_record_cid = mst.get(&key).await.ok().flatten();
|
||||
if prev_record_cid.is_none() {
|
||||
let prev_record_cid = mst.get(&key).await.map_err(|e| {
|
||||
error!("Failed to read prev record from MST: {}", e);
|
||||
ApiError::InternalError(Some("Failed to read MST".into()))
|
||||
})?;
|
||||
let Some(prev_record_cid) = prev_record_cid else {
|
||||
return Ok(Json(DeleteRecordOutput { commit: None }));
|
||||
}
|
||||
};
|
||||
|
||||
let new_mst = mst.delete(&key).await.map_err(|e| {
|
||||
error!("Failed to delete from MST: {:?}", e);
|
||||
error!("Failed to delete from MST: {}", e);
|
||||
ApiError::InternalError(Some("Failed to delete from MST".into()))
|
||||
})?;
|
||||
|
||||
let op = RecordOp::Delete {
|
||||
collection: input.collection.clone(),
|
||||
rkey: input.rkey.clone(),
|
||||
prev: prev_record_cid,
|
||||
prev: RecordCid::from(prev_record_cid),
|
||||
};
|
||||
|
||||
let modified_keys = [key];
|
||||
let deleted_uri = AtUri::from_parts(&did, &input.collection, &input.rkey);
|
||||
|
||||
let commit_result = finalize_repo_write(
|
||||
&state,
|
||||
@@ -95,19 +95,12 @@ pub async fn delete_record(
|
||||
ops: vec![op],
|
||||
modified_keys: &modified_keys,
|
||||
blob_cids: &[],
|
||||
backlinks_to_add: vec![],
|
||||
backlinks_to_remove: vec![deleted_uri],
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let deleted_uri = AtUri::from_parts(&did, &input.collection, &input.rkey);
|
||||
if let Err(e) = state
|
||||
.backlink_repo
|
||||
.remove_backlinks_by_uri(&deleted_uri)
|
||||
.await
|
||||
{
|
||||
error!("Failed to remove backlinks for {}: {}", deleted_uri, e);
|
||||
}
|
||||
|
||||
Ok(Json(DeleteRecordOutput {
|
||||
commit: Some(CommitInfo {
|
||||
cid: commit_result.commit_cid.to_string(),
|
||||
@@ -115,120 +108,3 @@ pub async fn delete_record(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn delete_record_internal(
|
||||
state: &AppState,
|
||||
did: &Did,
|
||||
user_id: Uuid,
|
||||
collection: &Nsid,
|
||||
rkey: &Rkey,
|
||||
) -> Result<(), CommitError> {
|
||||
use tranquil_pds::repo_ops::{CommitParams, RecordOp, commit_and_log};
|
||||
|
||||
let _write_lock = state.repo_write_locks.lock(user_id).await;
|
||||
|
||||
let root_cid_str = state
|
||||
.repo_repo
|
||||
.get_repo_root_cid_by_user_id(user_id)
|
||||
.await
|
||||
.map_err(|e| CommitError::DatabaseError(e.to_string()))?
|
||||
.ok_or(CommitError::RepoNotFound)?;
|
||||
|
||||
let current_root_cid =
|
||||
Cid::from_str(root_cid_str.as_str()).map_err(|e| CommitError::InvalidCid(e.to_string()))?;
|
||||
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
let commit_bytes = tracking_store
|
||||
.get(¤t_root_cid)
|
||||
.await
|
||||
.map_err(|e| CommitError::BlockStoreFailed(format!("{:?}", e)))?
|
||||
.ok_or(CommitError::BlockStoreFailed(
|
||||
"Commit block not found".into(),
|
||||
))?;
|
||||
|
||||
let commit = Commit::from_cbor(&commit_bytes)
|
||||
.map_err(|e| CommitError::CommitParseFailed(format!("{:?}", e)))?;
|
||||
|
||||
let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let key = format!("{}/{}", collection, rkey);
|
||||
|
||||
let prev_record_cid = mst
|
||||
.get(&key)
|
||||
.await
|
||||
.map_err(|e| CommitError::MstOperationFailed(format!("{:?}", e)))?;
|
||||
|
||||
let Some(prev_cid) = prev_record_cid else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let new_mst = mst
|
||||
.delete(&key)
|
||||
.await
|
||||
.map_err(|e| CommitError::MstOperationFailed(format!("{:?}", e)))?;
|
||||
|
||||
let new_mst_root = new_mst
|
||||
.persist()
|
||||
.await
|
||||
.map_err(|e| CommitError::MstOperationFailed(format!("{:?}", e)))?;
|
||||
|
||||
let op = RecordOp::Delete {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
prev: Some(prev_cid),
|
||||
};
|
||||
|
||||
let mut new_mst_blocks = std::collections::BTreeMap::new();
|
||||
let mut old_mst_blocks = std::collections::BTreeMap::new();
|
||||
|
||||
new_mst
|
||||
.blocks_for_path(&key, &mut new_mst_blocks)
|
||||
.await
|
||||
.map_err(|e| CommitError::MstOperationFailed(format!("{:?}", e)))?;
|
||||
|
||||
mst.blocks_for_path(&key, &mut old_mst_blocks)
|
||||
.await
|
||||
.map_err(|e| CommitError::MstOperationFailed(format!("{:?}", e)))?;
|
||||
|
||||
let obsolete_cids: Vec<Cid> = std::iter::once(current_root_cid)
|
||||
.chain(
|
||||
old_mst_blocks
|
||||
.keys()
|
||||
.filter(|cid| !new_mst_blocks.contains_key(*cid))
|
||||
.copied(),
|
||||
)
|
||||
.chain(std::iter::once(prev_cid))
|
||||
.collect();
|
||||
|
||||
let mut relevant_blocks = new_mst_blocks;
|
||||
relevant_blocks.extend(old_mst_blocks);
|
||||
|
||||
let written_cids: Vec<Cid> = tracking_store
|
||||
.get_all_relevant_cids()
|
||||
.into_iter()
|
||||
.chain(relevant_blocks.keys().copied())
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let written_cids_str: Vec<String> = written_cids.iter().map(ToString::to_string).collect();
|
||||
|
||||
commit_and_log(
|
||||
state,
|
||||
CommitParams {
|
||||
did,
|
||||
user_id,
|
||||
current_root_cid: Some(current_root_cid),
|
||||
prev_data_cid: Some(commit.data),
|
||||
new_mst_root,
|
||||
ops: vec![op],
|
||||
blocks_cids: &written_cids_str,
|
||||
blobs: &[],
|
||||
obsolete_cids,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ pub use pagination::PaginationDirection;
|
||||
pub use validation_mode::ValidationMode;
|
||||
|
||||
pub use batch::apply_writes;
|
||||
pub use delete::{DeleteRecordInput, delete_record, delete_record_internal};
|
||||
pub use delete::{DeleteRecordInput, delete_record};
|
||||
pub use read::{GetRecordInput, ListRecordsInput, ListRecordsOutput, get_record, list_records};
|
||||
pub use tranquil_pds::repo_ops::*;
|
||||
pub use write::{
|
||||
|
||||
@@ -60,12 +60,13 @@ pub async fn get_record(
|
||||
_headers: HeaderMap,
|
||||
Query(input): Query<GetRecordInput>,
|
||||
) -> Response {
|
||||
let user_id = match common::resolve_repo_user_id(state.user_repo.as_ref(), &input.repo).await {
|
||||
let user_id = match common::resolve_repo_user_id(state.repos.user.as_ref(), &input.repo).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
let record_row = state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.get_record_cid(user_id, &input.collection, &input.rkey)
|
||||
.await;
|
||||
let record_cid_link = match record_row {
|
||||
@@ -128,7 +129,7 @@ pub async fn list_records(
|
||||
State(state): State<AppState>,
|
||||
Query(input): Query<ListRecordsInput>,
|
||||
) -> Response {
|
||||
let user_id = match common::resolve_repo_user_id(state.user_repo.as_ref(), &input.repo).await {
|
||||
let user_id = match common::resolve_repo_user_id(state.repos.user.as_ref(), &input.repo).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
@@ -139,7 +140,8 @@ pub async fn list_records(
|
||||
.as_ref()
|
||||
.and_then(|c| c.parse::<tranquil_pds::types::Rkey>().ok());
|
||||
let rows = match state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.list_records(
|
||||
user_id,
|
||||
&input.collection,
|
||||
@@ -176,7 +178,7 @@ pub async fn list_records(
|
||||
};
|
||||
let records: Vec<Value> = parsed_rows
|
||||
.iter()
|
||||
.zip(blocks.into_iter())
|
||||
.zip(blocks)
|
||||
.filter_map(|((_, rkey, cid_str), block_opt)| {
|
||||
block_opt.and_then(|block| {
|
||||
serde_ipld_dagcbor::from_slice::<Ipld>(&block)
|
||||
|
||||
@@ -46,7 +46,8 @@ pub async fn prepare_repo_write<A: RepoScopeAction>(
|
||||
let _account_verified = require_verified_or_delegated(state, user).await?;
|
||||
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(principal_did.as_did())
|
||||
.await
|
||||
.log_db_err("fetching user for repo write")?
|
||||
@@ -128,7 +129,8 @@ pub async fn create_record(
|
||||
|
||||
if !backlinks.is_empty() {
|
||||
let conflicts = state
|
||||
.backlink_repo
|
||||
.repos
|
||||
.backlink
|
||||
.get_backlink_conflicts(user_id, &input.collection, &backlinks)
|
||||
.await
|
||||
.log_db_err("checking backlink conflicts")?;
|
||||
@@ -145,24 +147,32 @@ pub async fn create_record(
|
||||
|
||||
let prev_cid = match mst.get(&conflict_key).await {
|
||||
Ok(Some(cid)) => cid,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
mst = match mst.delete(&conflict_key).await {
|
||||
Ok(m) => m,
|
||||
Ok(None) => continue,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to delete conflict from MST {}: {:?}",
|
||||
"Failed to read conflict record from MST {}: {:?}",
|
||||
conflict_uri, e
|
||||
);
|
||||
continue;
|
||||
return Err(ApiError::InternalError(Some(
|
||||
"Failed to read conflicting record from MST".into(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
mst = mst.delete(&conflict_key).await.map_err(|e| {
|
||||
error!(
|
||||
"Failed to delete conflict from MST {}: {:?}",
|
||||
conflict_uri, e
|
||||
);
|
||||
ApiError::InternalError(Some(
|
||||
"Failed to delete conflicting record from MST".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
ops.push(RecordOp::Delete {
|
||||
collection: conflict_collection,
|
||||
rkey: conflict_rkey,
|
||||
prev: Some(prev_cid),
|
||||
prev: tranquil_pds::cid_types::RecordCid::from(prev_cid),
|
||||
});
|
||||
conflict_uris_to_cleanup.push(conflict_uri);
|
||||
}
|
||||
@@ -187,7 +197,7 @@ pub async fn create_record(
|
||||
ops.push(RecordOp::Create {
|
||||
collection: input.collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
cid: record_cid,
|
||||
cid: tranquil_pds::cid_types::RecordCid::from(record_cid),
|
||||
});
|
||||
|
||||
let modified_keys: Vec<String> = ops
|
||||
@@ -206,6 +216,9 @@ pub async fn create_record(
|
||||
.collect();
|
||||
let blob_cids = extract_blob_cids(&input.record);
|
||||
|
||||
let created_uri = AtUri::from_parts(&did, &input.collection, &rkey);
|
||||
let backlinks_to_add = extract_backlinks(&created_uri, &input.record);
|
||||
|
||||
let commit_result = finalize_repo_write(
|
||||
&state,
|
||||
ctx,
|
||||
@@ -224,31 +237,12 @@ pub async fn create_record(
|
||||
ops,
|
||||
modified_keys: &modified_keys,
|
||||
blob_cids: &blob_cids,
|
||||
backlinks_to_add,
|
||||
backlinks_to_remove: conflict_uris_to_cleanup,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let backlink_repo = state.backlink_repo.clone();
|
||||
futures::future::join_all(conflict_uris_to_cleanup.iter().map(|uri| {
|
||||
let backlink_repo = backlink_repo.clone();
|
||||
async move {
|
||||
if let Err(e) = backlink_repo.remove_backlinks_by_uri(uri).await {
|
||||
error!("Failed to remove backlinks for {}: {}", uri, e);
|
||||
}
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
|
||||
let created_uri = AtUri::from_parts(&did, &input.collection, &rkey);
|
||||
let backlinks = extract_backlinks(&created_uri, &input.record);
|
||||
if !backlinks.is_empty()
|
||||
&& let Err(e) = state.backlink_repo.add_backlinks(user_id, &backlinks).await
|
||||
{
|
||||
error!("Failed to add backlinks for {}: {}", created_uri, e);
|
||||
}
|
||||
|
||||
Ok(Json(CreateRecordOutput {
|
||||
uri: created_uri,
|
||||
cid: record_cid.to_string(),
|
||||
@@ -344,34 +338,38 @@ pub async fn put_record(
|
||||
}));
|
||||
}
|
||||
|
||||
let is_update = existing_cid.is_some();
|
||||
let new_mst = if is_update {
|
||||
mst.update(&key, record_cid)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to update MST".into())))?
|
||||
} else {
|
||||
mst.add(&key, record_cid)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to add to MST".into())))?
|
||||
};
|
||||
|
||||
let op = if is_update {
|
||||
RecordOp::Update {
|
||||
collection: input.collection.clone(),
|
||||
rkey: input.rkey.clone(),
|
||||
cid: record_cid,
|
||||
prev: existing_cid,
|
||||
let record_uri = AtUri::from_parts(&did, &input.collection, &input.rkey);
|
||||
let (new_mst, op, is_update, backlinks_to_remove) = match existing_cid {
|
||||
Some(prev_cid) => {
|
||||
let new_mst = mst
|
||||
.update(&key, record_cid)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to update MST".into())))?;
|
||||
let op = RecordOp::Update {
|
||||
collection: input.collection.clone(),
|
||||
rkey: input.rkey.clone(),
|
||||
cid: tranquil_pds::cid_types::RecordCid::from(record_cid),
|
||||
prev: tranquil_pds::cid_types::RecordCid::from(prev_cid),
|
||||
};
|
||||
(new_mst, op, true, vec![record_uri.clone()])
|
||||
}
|
||||
} else {
|
||||
RecordOp::Create {
|
||||
collection: input.collection.clone(),
|
||||
rkey: input.rkey.clone(),
|
||||
cid: record_cid,
|
||||
None => {
|
||||
let new_mst = mst
|
||||
.add(&key, record_cid)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to add to MST".into())))?;
|
||||
let op = RecordOp::Create {
|
||||
collection: input.collection.clone(),
|
||||
rkey: input.rkey.clone(),
|
||||
cid: tranquil_pds::cid_types::RecordCid::from(record_cid),
|
||||
};
|
||||
(new_mst, op, false, vec![])
|
||||
}
|
||||
};
|
||||
|
||||
let modified_keys = [key];
|
||||
let blob_cids = extract_blob_cids(&input.record);
|
||||
let backlinks_to_add = extract_backlinks(&record_uri, &input.record);
|
||||
|
||||
let commit_result = finalize_repo_write(
|
||||
&state,
|
||||
@@ -391,12 +389,14 @@ pub async fn put_record(
|
||||
ops: vec![op],
|
||||
modified_keys: &modified_keys,
|
||||
blob_cids: &blob_cids,
|
||||
backlinks_to_add,
|
||||
backlinks_to_remove,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(PutRecordOutput {
|
||||
uri: AtUri::from_parts(&did, &input.collection, &input.rkey),
|
||||
uri: record_uri,
|
||||
cid: record_cid.to_string(),
|
||||
commit: Some(CommitInfo {
|
||||
cid: commit_result.commit_cid.to_string(),
|
||||
|
||||
@@ -41,24 +41,27 @@ pub async fn check_account_status(
|
||||
) -> Result<Json<CheckAccountStatusOutput>, ApiError> {
|
||||
let did = &auth.did;
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.log_db_err("fetching user ID for account status")?
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
let is_active = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.is_account_active_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
let repo_info = state.repo_repo.get_repo(user_id).await.ok().flatten();
|
||||
let repo_info = state.repos.repo.get_repo(user_id).await.ok().flatten();
|
||||
let (repo_commit, repo_rev_from_db) = repo_info
|
||||
.map(|r| (r.repo_root_cid.to_string(), r.repo_rev))
|
||||
.unwrap_or_else(|| (String::new(), None));
|
||||
let block_count: i64 = state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.count_user_blocks(user_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
@@ -80,19 +83,21 @@ pub async fn check_account_status(
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let record_count: i64 = state.repo_repo.count_records(user_id).await.unwrap_or(0);
|
||||
let record_count: i64 = state.repos.repo.count_records(user_id).await.unwrap_or(0);
|
||||
let imported_blobs: i64 = state
|
||||
.blob_repo
|
||||
.repos
|
||||
.blob
|
||||
.count_blobs_by_user(user_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let expected_blobs: i64 = state
|
||||
.blob_repo
|
||||
.repos
|
||||
.blob
|
||||
.count_distinct_record_blobs(user_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let valid_did =
|
||||
is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), did).await;
|
||||
is_valid_did_for_service(state.repos.user.as_ref(), state.cache.clone(), did).await;
|
||||
Ok(Json(CheckAccountStatusOutput {
|
||||
activated: is_active,
|
||||
valid_did,
|
||||
@@ -319,7 +324,7 @@ pub async fn activate_account(
|
||||
);
|
||||
let did_validation_start = std::time::Instant::now();
|
||||
if let Err(e) = assert_valid_did_document_for_service(
|
||||
state.user_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.cache.clone(),
|
||||
&did,
|
||||
true,
|
||||
@@ -339,12 +344,18 @@ pub async fn activate_account(
|
||||
did_validation_start.elapsed()
|
||||
);
|
||||
|
||||
let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten();
|
||||
let handle = state
|
||||
.repos
|
||||
.user
|
||||
.get_handle_by_did(&did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: Activating account did={} handle={:?}",
|
||||
did, handle
|
||||
);
|
||||
let result = state.user_repo.activate_account(&did).await;
|
||||
let result = state.repos.user.activate_account(&did).await;
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
@@ -365,12 +376,20 @@ pub async fn activate_account(
|
||||
.cache
|
||||
.delete(&tranquil_pds::cache_keys::plc_data_key(&did))
|
||||
.await;
|
||||
if state.did_resolver.refresh_did(did.as_str()).await.is_none() {
|
||||
if state.did_resolver.refresh_did(did.as_str()).await.is_err() {
|
||||
warn!(
|
||||
"[MIGRATION] activateAccount: Failed to refresh DID cache for {}",
|
||||
did
|
||||
);
|
||||
}
|
||||
if let Err(e) = state
|
||||
.repos
|
||||
.repo
|
||||
.update_repo_status(&did, None, None, Some(false))
|
||||
.await
|
||||
{
|
||||
warn!("failed to sync activation to repo backend: {e:?}");
|
||||
}
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: Sequencing account event (active=true) for did={}",
|
||||
did
|
||||
@@ -406,7 +425,8 @@ pub async fn activate_account(
|
||||
info!("[MIGRATION] activateAccount: Identity event sequenced successfully");
|
||||
}
|
||||
let repo_root = state
|
||||
.repo_repo
|
||||
.repos
|
||||
.repo
|
||||
.get_repo_root_by_did(&did)
|
||||
.await
|
||||
.ok()
|
||||
@@ -480,9 +500,19 @@ pub async fn deactivate_account(
|
||||
|
||||
let did = auth.did.clone();
|
||||
|
||||
let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten();
|
||||
let handle = state
|
||||
.repos
|
||||
.user
|
||||
.get_handle_by_did(&did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let result = state.user_repo.deactivate_account(&did, delete_after).await;
|
||||
let result = state
|
||||
.repos
|
||||
.user
|
||||
.deactivate_account(&did, delete_after)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(true) => {
|
||||
@@ -492,6 +522,14 @@ pub async fn deactivate_account(
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(h))
|
||||
.await;
|
||||
}
|
||||
if let Err(e) = state
|
||||
.repos
|
||||
.repo
|
||||
.update_repo_status(&did, None, None, Some(true))
|
||||
.await
|
||||
{
|
||||
warn!("failed to sync deactivation to repo backend: {e:?}");
|
||||
}
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
&did,
|
||||
@@ -518,7 +556,8 @@ pub async fn request_account_delete(
|
||||
let session_mfa = require_legacy_session_mfa(&state, &auth).await?;
|
||||
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(session_mfa.did())
|
||||
.await
|
||||
.ok()
|
||||
@@ -527,14 +566,15 @@ pub async fn request_account_delete(
|
||||
let confirmation_token = Uuid::new_v4().to_string();
|
||||
let expires_at = Utc::now() + Duration::minutes(15);
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.create_deletion_request(&confirmation_token, session_mfa.did(), expires_at)
|
||||
.await
|
||||
.log_db_err("creating deletion token")?;
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_account_deletion(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
&confirmation_token,
|
||||
hostname,
|
||||
@@ -572,7 +612,8 @@ pub async fn delete_account(
|
||||
return Err(ApiError::InvalidToken(Some("token is required".into())));
|
||||
}
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_for_deletion(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -582,7 +623,7 @@ pub async fn delete_account(
|
||||
.ok_or(ApiError::InvalidRequest("account not found".into()))?;
|
||||
let (user_id, password_hash, handle) = (user.id, user.password_hash, user.handle);
|
||||
if crate::common::verify_credential(
|
||||
state.session_repo.as_ref(),
|
||||
state.repos.session.as_ref(),
|
||||
user_id,
|
||||
password,
|
||||
password_hash.as_deref(),
|
||||
@@ -595,7 +636,8 @@ pub async fn delete_account(
|
||||
)));
|
||||
}
|
||||
let deletion_request = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_deletion_request(token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -611,11 +653,12 @@ pub async fn delete_account(
|
||||
)));
|
||||
}
|
||||
if Utc::now() > deletion_request.expires_at {
|
||||
let _ = state.infra_repo.delete_deletion_request(token).await;
|
||||
let _ = state.repos.infra.delete_deletion_request(token).await;
|
||||
return Err(ApiError::ExpiredToken(None));
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.delete_account_complete(user_id, did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -630,7 +673,7 @@ pub async fn delete_account(
|
||||
.await;
|
||||
match account_seq {
|
||||
Ok(seq) => {
|
||||
if let Err(e) = state.repo_repo.delete_sequences_except(did, seq).await {
|
||||
if let Err(e) = state.repos.repo.delete_sequences_except(did, seq).await {
|
||||
warn!(
|
||||
"Failed to cleanup sequences for deleted account {}: {}",
|
||||
did, e
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::error;
|
||||
use tranquil_db_traits::AppPasswordCreate;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
@@ -32,14 +31,16 @@ pub async fn list_app_passwords(
|
||||
auth: Auth<Permissive>,
|
||||
) -> Result<Json<ListAppPasswordsOutput>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting user")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let rows = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.list_app_passwords(user.id)
|
||||
.await
|
||||
.log_db_err("listing app passwords")?;
|
||||
@@ -84,7 +85,8 @@ pub async fn create_app_password(
|
||||
Json(input): Json<CreateAppPasswordInput>,
|
||||
) -> Result<Json<CreateAppPasswordOutput>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting user")?
|
||||
@@ -96,7 +98,8 @@ pub async fn create_app_password(
|
||||
}
|
||||
|
||||
if state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.get_app_password_by_name(user.id, name)
|
||||
.await
|
||||
.log_db_err("checking app password")?
|
||||
@@ -107,7 +110,8 @@ pub async fn create_app_password(
|
||||
|
||||
let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did {
|
||||
let grant = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.get_delegation(&auth.did, controller)
|
||||
.await
|
||||
.ok()
|
||||
@@ -133,18 +137,7 @@ pub async fn create_app_password(
|
||||
|
||||
let password = generate_app_password();
|
||||
|
||||
let password_clone = password.clone();
|
||||
let password_hash =
|
||||
tokio::task::spawn_blocking(move || bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let password_hash = crate::common::hash_password_async(&password).await?;
|
||||
|
||||
let privilege = tranquil_db_traits::AppPasswordPrivilege::from_privileged_flag(
|
||||
input.privileged.unwrap_or(false),
|
||||
@@ -161,14 +154,16 @@ pub async fn create_app_password(
|
||||
};
|
||||
|
||||
state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.create_app_password(&create_data)
|
||||
.await
|
||||
.log_db_err("creating app password")?;
|
||||
|
||||
if let Some(ref controller) = controller_did {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&auth.did,
|
||||
controller,
|
||||
@@ -204,7 +199,8 @@ pub async fn revoke_app_password(
|
||||
Json(input): Json<RevokeAppPasswordInput>,
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting user")?
|
||||
@@ -216,13 +212,15 @@ pub async fn revoke_app_password(
|
||||
}
|
||||
|
||||
let sessions_to_invalidate = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.get_session_jtis_by_app_password(&auth.did, name)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.delete_sessions_by_app_password(&auth.did, name)
|
||||
.await
|
||||
.log_db_err("revoking sessions for app password")?;
|
||||
@@ -237,7 +235,8 @@ pub async fn revoke_app_password(
|
||||
.await;
|
||||
|
||||
state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.delete_app_password(user.id, name)
|
||||
.await
|
||||
.log_db_err("revoking app password")?;
|
||||
|
||||
@@ -22,10 +22,6 @@ use tranquil_pds::state::AppState;
|
||||
|
||||
const EMAIL_UPDATE_TTL: Duration = Duration::from_secs(30 * 60);
|
||||
|
||||
fn email_update_cache_key(did: &str) -> String {
|
||||
tranquil_pds::cache_keys::email_update_key(did)
|
||||
}
|
||||
|
||||
fn hash_token(token: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token.as_bytes());
|
||||
@@ -39,6 +35,16 @@ struct PendingEmailUpdate {
|
||||
authorized: bool,
|
||||
}
|
||||
|
||||
async fn get_pending_email_update(
|
||||
cache: &dyn tranquil_pds::cache::Cache,
|
||||
did: &str,
|
||||
) -> Option<PendingEmailUpdate> {
|
||||
cache
|
||||
.get(&tranquil_pds::cache_keys::email_update_key(did))
|
||||
.await
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RequestEmailUpdateInput {
|
||||
@@ -55,7 +61,8 @@ pub async fn request_email_update(
|
||||
auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?;
|
||||
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_email_info_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting email info")?
|
||||
@@ -92,7 +99,7 @@ pub async fn request_email_update(
|
||||
authorized: false,
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&pending) {
|
||||
let cache_key = email_update_cache_key(&auth.did);
|
||||
let cache_key = tranquil_pds::cache_keys::email_update_key(&auth.did);
|
||||
if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await {
|
||||
warn!("Failed to cache pending email update: {:?}", e);
|
||||
}
|
||||
@@ -102,8 +109,8 @@ pub async fn request_email_update(
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_short_token_email(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user.id,
|
||||
&token,
|
||||
hostname,
|
||||
@@ -135,7 +142,8 @@ pub async fn confirm_email(
|
||||
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_email_info_by_did(did)
|
||||
.await
|
||||
.log_db_err("getting email info")?
|
||||
@@ -179,7 +187,8 @@ pub async fn confirm_email(
|
||||
}
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_email_verified(user.id, true)
|
||||
.await
|
||||
.log_db_err("confirming email")?;
|
||||
@@ -206,7 +215,8 @@ pub async fn update_email(
|
||||
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_email_info_by_did(did)
|
||||
.await
|
||||
.log_db_err("getting email info")?
|
||||
@@ -253,7 +263,8 @@ pub async fn update_email(
|
||||
}
|
||||
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_account_preference(user_id, "email_auth_factor", json!(email_auth_factor))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -267,7 +278,7 @@ pub async fn update_email(
|
||||
if email_verified {
|
||||
let mut authorized_via_link = false;
|
||||
|
||||
let cache_key = email_update_cache_key(did);
|
||||
let cache_key = tranquil_pds::cache_keys::email_update_key(did);
|
||||
if let Some(pending_json) = state.cache.get(&cache_key).await
|
||||
&& let Ok(pending) = serde_json::from_str::<PendingEmailUpdate>(&pending_json)
|
||||
&& pending.authorized
|
||||
@@ -336,7 +347,8 @@ pub async fn update_email(
|
||||
}
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_email(user_id, &new_email)
|
||||
.await
|
||||
.log_db_err("updating email")?;
|
||||
@@ -350,8 +362,8 @@ pub async fn update_email(
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
tranquil_db_traits::CommsChannel::Email,
|
||||
&new_email,
|
||||
@@ -364,7 +376,8 @@ pub async fn update_email(
|
||||
}
|
||||
|
||||
if let Err(e) = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.upsert_account_preference(
|
||||
user_id,
|
||||
"email_auth_factor",
|
||||
@@ -390,7 +403,8 @@ pub async fn check_email_verified(
|
||||
Json(input): Json<CheckEmailVerifiedInput>,
|
||||
) -> Result<Json<VerifiedResponse>, ApiError> {
|
||||
let verified = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.check_email_verified_by_identifier(&input.identifier)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -414,7 +428,8 @@ pub async fn check_channel_verified(
|
||||
Json(input): Json<CheckChannelVerifiedInput>,
|
||||
) -> Result<Json<VerifiedResponse>, ApiError> {
|
||||
let verified = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.check_channel_verified_by_did(&input.did, input.channel)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -470,9 +485,9 @@ pub async fn authorize_email_update(
|
||||
let did = token_data.did;
|
||||
info!("authorize_email_update: token valid for did={}", did);
|
||||
|
||||
let cache_key = email_update_cache_key(&did);
|
||||
let pending_json = match state.cache.get(&cache_key).await {
|
||||
Some(json) => json,
|
||||
let cache_key = tranquil_pds::cache_keys::email_update_key(&did);
|
||||
let mut pending = match get_pending_email_update(state.cache.as_ref(), &did).await {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
warn!(
|
||||
"authorize_email_update: no pending email update in cache for did={}",
|
||||
@@ -483,13 +498,6 @@ pub async fn authorize_email_update(
|
||||
}
|
||||
};
|
||||
|
||||
let mut pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let token_hash = hash_token(&query.token);
|
||||
if pending
|
||||
.token_hash
|
||||
@@ -528,9 +536,8 @@ pub async fn check_email_update_status(
|
||||
) -> Result<Json<EmailUpdateStatusOutput>, ApiError> {
|
||||
auth.check_account_scope(AccountAttr::Email, AccountAction::Read)?;
|
||||
|
||||
let cache_key = email_update_cache_key(&auth.did);
|
||||
let pending_json = match state.cache.get(&cache_key).await {
|
||||
Some(json) => json,
|
||||
let pending = match get_pending_email_update(state.cache.as_ref(), &auth.did).await {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return Ok(Json(EmailUpdateStatusOutput {
|
||||
pending: false,
|
||||
@@ -540,17 +547,6 @@ pub async fn check_email_update_status(
|
||||
}
|
||||
};
|
||||
|
||||
let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return Ok(Json(EmailUpdateStatusOutput {
|
||||
pending: false,
|
||||
authorized: false,
|
||||
new_email: None,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(EmailUpdateStatusOutput {
|
||||
pending: true,
|
||||
authorized: pending.authorized,
|
||||
@@ -574,7 +570,8 @@ pub async fn check_email_in_use(
|
||||
}
|
||||
|
||||
let count = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.count_accounts_by_email(&email)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use axum::{Json, extract::State};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
use tranquil_pds::api::ApiError;
|
||||
@@ -7,24 +6,7 @@ use tranquil_pds::api::error::DbResultExt;
|
||||
use tranquil_pds::auth::{Admin, Auth, NotTakendown};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::Did;
|
||||
|
||||
const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
|
||||
|
||||
pub(crate) fn gen_random_token() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let gen_segment = |rng: &mut rand::rngs::ThreadRng, len: usize| -> String {
|
||||
(0..len)
|
||||
.map(|_| BASE32_ALPHABET[rng.gen_range(0..32)] as char)
|
||||
.collect()
|
||||
};
|
||||
format!("{}-{}", gen_segment(&mut rng, 5), gen_segment(&mut rng, 5))
|
||||
}
|
||||
|
||||
pub fn gen_invite_code() -> String {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_prefix = hostname.replace('.', "-");
|
||||
format!("{}-{}", hostname_prefix, gen_random_token())
|
||||
}
|
||||
use tranquil_pds::util::gen_invite_code;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -58,7 +40,8 @@ pub async fn create_invite_code(
|
||||
let code = gen_invite_code();
|
||||
|
||||
match state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.create_invite_code(&code, input.use_count, Some(&for_account))
|
||||
.await
|
||||
{
|
||||
@@ -115,7 +98,8 @@ pub async fn create_invite_codes(
|
||||
};
|
||||
|
||||
let admin_user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_any_admin_user_id()
|
||||
.await
|
||||
.log_db_err("looking up admin user")?
|
||||
@@ -125,7 +109,7 @@ pub async fn create_invite_codes(
|
||||
})?;
|
||||
|
||||
let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| {
|
||||
let infra_repo = state.infra_repo.clone();
|
||||
let infra_repo = state.repos.infra.clone();
|
||||
let use_count = input.use_count;
|
||||
async move {
|
||||
let codes: Vec<String> = (0..code_count).map(|_| gen_invite_code()).collect();
|
||||
@@ -192,7 +176,8 @@ pub async fn get_account_invite_codes(
|
||||
let include_used = params.include_used.unwrap_or(true);
|
||||
|
||||
let codes_info = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_codes_for_account(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching invite codes")?;
|
||||
@@ -203,7 +188,7 @@ pub async fn get_account_invite_codes(
|
||||
.collect();
|
||||
|
||||
let codes = futures::future::join_all(filtered_codes.into_iter().map(|info| {
|
||||
let infra_repo = state.infra_repo.clone();
|
||||
let infra_repo = state.repos.infra.clone();
|
||||
async move {
|
||||
let uses = infra_repo
|
||||
.get_invite_code_uses(&info.code)
|
||||
|
||||
@@ -9,7 +9,7 @@ use tracing::error;
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
pub async fn get_logo(State(state): State<AppState>) -> Response {
|
||||
let logo_cid = match state.infra_repo.get_server_config("logo_cid").await {
|
||||
let logo_cid = match state.repos.infra.get_server_config("logo_cid").await {
|
||||
Ok(cid) => cid,
|
||||
Err(e) => {
|
||||
error!("DB error fetching logo_cid: {:?}", e);
|
||||
@@ -26,7 +26,7 @@ pub async fn get_logo(State(state): State<AppState>) -> Response {
|
||||
Err(_) => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
|
||||
let metadata = match state.blob_repo.get_blob_metadata(&cid).await {
|
||||
let metadata = match state.repos.blob.get_blob_metadata(&cid).await {
|
||||
Ok(Some(m)) => m,
|
||||
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ fn get_available_comms_channels() -> Vec<CommsChannel> {
|
||||
if cfg.telegram.bot_token.is_some() {
|
||||
channels.push(CommsChannel::Telegram);
|
||||
}
|
||||
if cfg.signal.sender_number.is_some() {
|
||||
if cfg.signal.enabled {
|
||||
channels.push(CommsChannel::Signal);
|
||||
}
|
||||
channels
|
||||
@@ -98,7 +98,7 @@ pub struct HealthOutput {
|
||||
}
|
||||
|
||||
pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
|
||||
match state.infra_repo.health_check().await {
|
||||
match state.repos.infra.health_check().await {
|
||||
Ok(true) => (
|
||||
StatusCode::OK,
|
||||
Json(HealthOutput {
|
||||
|
||||
@@ -42,7 +42,8 @@ pub async fn update_did_document(
|
||||
}
|
||||
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_for_did_doc(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting user")?
|
||||
@@ -97,7 +98,8 @@ pub async fn update_did_document(
|
||||
let also_known_as: Option<Vec<String>> = input.also_known_as.clone();
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.upsert_did_web_overrides(user.id, verification_methods_json, also_known_as)
|
||||
.await
|
||||
.log_db_err("upserting did_web_overrides")?;
|
||||
@@ -105,7 +107,8 @@ pub async fn update_did_document(
|
||||
if let Some(ref endpoint) = input.service_endpoint {
|
||||
let endpoint_clean = endpoint.trim().trim_end_matches('/');
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_migrated_to_pds(&auth.did, endpoint_clean)
|
||||
.await
|
||||
.log_db_err("updating service endpoint")?;
|
||||
@@ -139,7 +142,7 @@ pub async fn get_did_document(
|
||||
async fn build_did_document(state: &AppState, did: &tranquil_pds::types::Did) -> serde_json::Value {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
|
||||
let user = match state.user_repo.get_user_for_did_doc_build(did).await {
|
||||
let user = match state.repos.user.get_user_for_did_doc_build(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
return json!({
|
||||
@@ -149,7 +152,8 @@ async fn build_did_document(state: &AppState, did: &tranquil_pds::types::Did) ->
|
||||
};
|
||||
|
||||
let overrides = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_did_web_overrides(user.id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -193,7 +197,8 @@ async fn build_did_document(state: &AppState, did: &tranquil_pds::types::Did) ->
|
||||
}
|
||||
|
||||
let key_info = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_key_by_id(user.id)
|
||||
.await
|
||||
.ok()
|
||||
|
||||
@@ -120,7 +120,7 @@ pub async fn create_passkey_account(
|
||||
}
|
||||
|
||||
let is_bootstrap = state.bootstrap_invite_code.is_some()
|
||||
&& state.user_repo.count_users().await.unwrap_or(1) == 0;
|
||||
&& state.repos.user.count_users().await.unwrap_or(1) == 0;
|
||||
|
||||
let _validated_invite_code = if is_bootstrap {
|
||||
match input.invite_code.as_deref() {
|
||||
@@ -128,7 +128,7 @@ pub async fn create_passkey_account(
|
||||
_ => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
} else if let Some(ref code) = input.invite_code {
|
||||
match state.infra_repo.validate_invite_code(code).await {
|
||||
match state.repos.infra.validate_invite_code(code).await {
|
||||
Ok(validated) => Some(validated),
|
||||
Err(_) => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
@@ -351,7 +351,7 @@ pub async fn create_passkey_account(
|
||||
birthdate_pref,
|
||||
};
|
||||
|
||||
let create_result = match state.user_repo.create_passkey_account(&create_input).await {
|
||||
let create_result = match state.repos.user.create_passkey_account(&create_input).await {
|
||||
Ok(r) => r,
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return Err(ApiError::HandleNotAvailable(None));
|
||||
@@ -393,19 +393,19 @@ pub async fn create_passkey_account(
|
||||
Ok(token_meta) => {
|
||||
let refresh_jti = uuid::Uuid::new_v4().to_string();
|
||||
let refresh_expires = chrono::Utc::now() + chrono::Duration::hours(24);
|
||||
let session_data = tranquil_db::SessionTokenCreate {
|
||||
let session_data = tranquil_db_traits::SessionTokenCreate {
|
||||
did: did_typed.clone(),
|
||||
access_jti: token_meta.jti.clone(),
|
||||
refresh_jti,
|
||||
access_expires_at: token_meta.expires_at,
|
||||
refresh_expires_at: refresh_expires,
|
||||
login_type: tranquil_db::LoginType::Modern,
|
||||
login_type: tranquil_db_traits::LoginType::Modern,
|
||||
mfa_verified: false,
|
||||
scope: Some("transition:generic".to_string()),
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
if let Err(e) = state.session_repo.create_session(&session_data).await {
|
||||
if let Err(e) = state.repos.session.create_session(&session_data).await {
|
||||
warn!(did = %did, "Failed to insert migration session: {:?}", e);
|
||||
}
|
||||
info!(did = %did, "Generated migration access token for BYOD passkey account");
|
||||
@@ -451,7 +451,12 @@ pub async fn complete_passkey_setup(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<CompletePasskeySetupInput>,
|
||||
) -> Result<Json<CompletePasskeySetupOutput>, ApiError> {
|
||||
let user = match state.user_repo.get_user_for_passkey_setup(&input.did).await {
|
||||
let user = match state
|
||||
.repos
|
||||
.user
|
||||
.get_user_for_passkey_setup(&input.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return Err(ApiError::AccountNotFound);
|
||||
@@ -484,7 +489,8 @@ pub async fn complete_passkey_setup(
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
let reg_state = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.load_webauthn_challenge(&input.did, WebauthnChallengeType::Registration)
|
||||
.await
|
||||
{
|
||||
@@ -530,7 +536,8 @@ pub async fn complete_passkey_setup(
|
||||
}
|
||||
};
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.save_passkey(
|
||||
&input.did,
|
||||
&credential_id,
|
||||
@@ -553,13 +560,14 @@ pub async fn complete_passkey_setup(
|
||||
app_password_name: app_password_name.clone(),
|
||||
app_password_hash: password_hash,
|
||||
};
|
||||
if let Err(e) = state.user_repo.complete_passkey_setup(&setup_input).await {
|
||||
if let Err(e) = state.repos.user.complete_passkey_setup(&setup_input).await {
|
||||
error!("Error completing passkey setup: {:?}", e);
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
|
||||
let _ = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.delete_webauthn_challenge(&input.did, WebauthnChallengeType::Registration)
|
||||
.await;
|
||||
|
||||
@@ -577,7 +585,12 @@ pub async fn start_passkey_registration_for_setup(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<StartPasskeyRegistrationInput>,
|
||||
) -> Result<Json<OptionsResponse<serde_json::Value>>, ApiError> {
|
||||
let user = match state.user_repo.get_user_for_passkey_setup(&input.did).await {
|
||||
let user = match state
|
||||
.repos
|
||||
.user
|
||||
.get_user_for_passkey_setup(&input.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return Err(ApiError::AccountNotFound);
|
||||
@@ -610,7 +623,8 @@ pub async fn start_passkey_registration_for_setup(
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
let existing_passkeys = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_passkeys_for_user(&input.did)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -643,7 +657,8 @@ pub async fn start_passkey_registration_for_setup(
|
||||
}
|
||||
};
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.save_webauthn_challenge(&input.did, WebauthnChallengeType::Registration, &state_json)
|
||||
.await
|
||||
{
|
||||
@@ -682,7 +697,8 @@ pub async fn request_passkey_recovery(
|
||||
NormalizedLoginIdentifier::normalize(&input.email, hostname_for_handles);
|
||||
|
||||
let user = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_for_passkey_recovery(identifier, normalized_handle.as_str())
|
||||
.await
|
||||
{
|
||||
@@ -697,7 +713,8 @@ pub async fn request_passkey_recovery(
|
||||
let expires_at = Utc::now() + Duration::hours(1);
|
||||
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_recovery_token(&user.did, &recovery_token_hash, expires_at)
|
||||
.await
|
||||
{
|
||||
@@ -714,8 +731,8 @@ pub async fn request_passkey_recovery(
|
||||
);
|
||||
|
||||
let _ = tranquil_pds::comms::comms_repo::enqueue_passkey_recovery(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user.id,
|
||||
&recovery_url,
|
||||
hostname,
|
||||
@@ -742,7 +759,7 @@ pub async fn recover_passkey_account(
|
||||
return Err(ApiError::InvalidRequest(e.to_string()));
|
||||
}
|
||||
|
||||
let user = match state.user_repo.get_user_for_recovery(&input.did).await {
|
||||
let user = match state.repos.user.get_user_for_recovery(&input.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
_ => {
|
||||
return Err(ApiError::InvalidRecoveryLink);
|
||||
@@ -771,7 +788,8 @@ pub async fn recover_passkey_account(
|
||||
password_hash,
|
||||
};
|
||||
let result = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.recover_passkey_account(&recover_input)
|
||||
.await
|
||||
{
|
||||
@@ -785,11 +803,12 @@ pub async fn recover_passkey_account(
|
||||
if result.passkeys_deleted > 0 {
|
||||
info!(did = %input.did, count = result.passkeys_deleted, "Deleted lost passkeys during account recovery");
|
||||
}
|
||||
if let Ok(Some(prefs)) = state.user_repo.get_comms_prefs(user.id).await {
|
||||
if let Ok(Some(prefs)) = state.repos.user.get_comms_prefs(user.id).await {
|
||||
let actual_channel =
|
||||
tranquil_pds::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel);
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_channel_verified(&input.did, actual_channel)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -28,14 +28,16 @@ pub async fn start_passkey_registration(
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
let handle = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_handle_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching user")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let existing_passkeys = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_passkeys_for_user(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching existing passkeys")?;
|
||||
@@ -60,7 +62,8 @@ pub async fn start_passkey_registration(
|
||||
})?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.save_webauthn_challenge(&auth.did, WebauthnChallengeType::Registration, &state_json)
|
||||
.await
|
||||
.log_db_err("saving registration state")?;
|
||||
@@ -94,7 +97,8 @@ pub async fn finish_passkey_registration(
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
let reg_state_json = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.load_webauthn_challenge(&auth.did, WebauthnChallengeType::Registration)
|
||||
.await
|
||||
.log_db_err("loading registration state")?
|
||||
@@ -125,7 +129,8 @@ pub async fn finish_passkey_registration(
|
||||
})?;
|
||||
|
||||
let passkey_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.save_passkey(
|
||||
&auth.did,
|
||||
passkey.cred_id(),
|
||||
@@ -136,7 +141,8 @@ pub async fn finish_passkey_registration(
|
||||
.log_db_err("saving passkey")?;
|
||||
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.delete_webauthn_challenge(&auth.did, WebauthnChallengeType::Registration)
|
||||
.await
|
||||
{
|
||||
@@ -177,7 +183,8 @@ pub async fn list_passkeys(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<ListPasskeysOutput>, ApiError> {
|
||||
let passkeys = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_passkeys_for_user(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching passkeys")?;
|
||||
@@ -215,7 +222,7 @@ pub async fn delete_passkey(
|
||||
|
||||
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
|
||||
|
||||
match state.user_repo.delete_passkey(id, reauth_mfa.did()).await {
|
||||
match state.repos.user.delete_passkey(id, reauth_mfa.did()).await {
|
||||
Ok(true) => {
|
||||
info!(did = %session_mfa.did(), passkey_id = %id, "Passkey deleted");
|
||||
Ok(Json(EmptyResponse {}))
|
||||
@@ -243,7 +250,8 @@ pub async fn update_passkey(
|
||||
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_passkey_name(id, &auth.did, &input.friendly_name)
|
||||
.await
|
||||
{
|
||||
@@ -260,5 +268,5 @@ pub async fn update_passkey(
|
||||
}
|
||||
|
||||
pub async fn has_passkeys_for_user(state: &AppState, did: &tranquil_pds::types::Did) -> bool {
|
||||
state.user_repo.has_passkeys(did).await.unwrap_or(false)
|
||||
state.repos.user.has_passkeys(did).await.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use axum::{Json, extract::State};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use chrono::{Duration, Utc};
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, info, warn};
|
||||
@@ -42,7 +41,7 @@ pub async fn request_password_reset(
|
||||
let normalized_handle = NormalizedLoginIdentifier::normalize(identifier, hostname_for_handles);
|
||||
|
||||
let multiple_accounts_warning = if is_email_lookup {
|
||||
match state.user_repo.count_accounts_by_email(normalized).await {
|
||||
match state.repos.user.count_accounts_by_email(normalized).await {
|
||||
Ok(count) if count > 1 => Some(count),
|
||||
_ => None,
|
||||
}
|
||||
@@ -51,7 +50,8 @@ pub async fn request_password_reset(
|
||||
};
|
||||
|
||||
let user_id = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_email_or_handle(normalized, normalized_handle.as_str())
|
||||
.await
|
||||
{
|
||||
@@ -73,7 +73,8 @@ pub async fn request_password_reset(
|
||||
let code = generate_reset_code();
|
||||
let expires_at = Utc::now() + Duration::minutes(10);
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_password_reset_code(user_id, &code, expires_at)
|
||||
.await
|
||||
{
|
||||
@@ -82,8 +83,8 @@ pub async fn request_password_reset(
|
||||
}
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_password_reset(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
&code,
|
||||
hostname,
|
||||
@@ -132,7 +133,7 @@ pub async fn reset_password(
|
||||
if let Err(e) = validate_password(password) {
|
||||
return Err(ApiError::InvalidRequest(e.to_string()));
|
||||
}
|
||||
let user = match state.user_repo.get_user_by_reset_code(token).await {
|
||||
let user = match state.repos.user.get_user_by_reset_code(token).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
@@ -147,26 +148,15 @@ pub async fn reset_password(
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
};
|
||||
if Utc::now() > exp {
|
||||
if let Err(e) = state.user_repo.clear_password_reset_code(user_id).await {
|
||||
if let Err(e) = state.repos.user.clear_password_reset_code(user_id).await {
|
||||
error!("Failed to clear expired reset code: {:?}", e);
|
||||
}
|
||||
return Err(ApiError::ExpiredToken(None));
|
||||
}
|
||||
let password_clone = password.to_string();
|
||||
let password_hash =
|
||||
match tokio::task::spawn_blocking(move || hash(password_clone, DEFAULT_COST)).await {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(e)) => {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
let password_hash = crate::common::hash_password_async(password).await?;
|
||||
let result = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.reset_password_with_sessions(user_id, &password_hash)
|
||||
.await
|
||||
{
|
||||
@@ -189,11 +179,12 @@ pub async fn reset_password(
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
if let Ok(Some(prefs)) = state.user_repo.get_comms_prefs(user_id).await {
|
||||
if let Ok(Some(prefs)) = state.repos.user.get_comms_prefs(user_id).await {
|
||||
let actual_channel =
|
||||
tranquil_pds::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel);
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_channel_verified(&user.did, actual_channel)
|
||||
.await
|
||||
{
|
||||
@@ -238,26 +229,18 @@ pub async fn change_password(
|
||||
let password_mfa = verify_password_mfa(&state, &auth, &input.current_password).await?;
|
||||
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_and_password_hash_by_did(password_mfa.did())
|
||||
.await
|
||||
.log_db_err("in change_password")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let new_password_clone = input.new_password.to_string();
|
||||
let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let new_hash = crate::common::hash_password_async(&input.new_password).await?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_password_hash(user.id, &new_hash)
|
||||
.await
|
||||
.log_db_err("updating password")?;
|
||||
@@ -271,7 +254,8 @@ pub async fn get_password_status(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<HasPasswordResponse>, ApiError> {
|
||||
let has = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.has_password_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("checking password status")?
|
||||
@@ -288,7 +272,8 @@ pub async fn remove_password(
|
||||
let reauth_mfa = require_reauth_window(&state, &auth).await?;
|
||||
|
||||
let has_passkeys = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.has_passkeys(reauth_mfa.did())
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
@@ -299,7 +284,8 @@ pub async fn remove_password(
|
||||
}
|
||||
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_password_info_by_did(reauth_mfa.did())
|
||||
.await
|
||||
.log_db_err("getting password info")?
|
||||
@@ -312,7 +298,8 @@ pub async fn remove_password(
|
||||
}
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.remove_user_password(user.id)
|
||||
.await
|
||||
.log_db_err("removing password")?;
|
||||
@@ -345,7 +332,8 @@ pub async fn set_password(
|
||||
let did = reauth_mfa.as_ref().map(|m| m.did()).unwrap_or(&auth.did);
|
||||
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_password_info_by_did(did)
|
||||
.await
|
||||
.log_db_err("getting password info")?
|
||||
@@ -357,20 +345,11 @@ pub async fn set_password(
|
||||
));
|
||||
}
|
||||
|
||||
let new_password_clone = new_password.to_string();
|
||||
let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let new_hash = crate::common::hash_password_async(new_password).await?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_new_user_password(user.id, &new_hash)
|
||||
.await
|
||||
.log_db_err("setting password")?;
|
||||
|
||||
@@ -33,13 +33,14 @@ pub async fn get_reauth_status(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<ReauthStatusOutput>, ApiError> {
|
||||
let last_reauth_at = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.get_last_reauth_at(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting last reauth")?;
|
||||
|
||||
let reauth_required = is_reauth_required(last_reauth_at);
|
||||
let available_methods = get_available_reauth_methods(&*state.user_repo, &auth.did).await;
|
||||
let available_methods = get_available_reauth_methods(&*state.repos.user, &auth.did).await;
|
||||
|
||||
Ok(Json(ReauthStatusOutput {
|
||||
last_reauth_at,
|
||||
@@ -66,7 +67,8 @@ pub async fn reauth_password(
|
||||
Json(input): Json<PasswordReauthInput>,
|
||||
) -> Result<Json<ReauthOutput>, ApiError> {
|
||||
let password_hash = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_password_hash_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching password hash")?
|
||||
@@ -76,7 +78,8 @@ pub async fn reauth_password(
|
||||
|
||||
if !password_valid {
|
||||
let app_password_hashes = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.get_app_password_hashes_by_did(&auth.did)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -91,7 +94,7 @@ pub async fn reauth_password(
|
||||
}
|
||||
}
|
||||
|
||||
let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did)
|
||||
let reauthed_at = update_last_reauth_cached(&*state.repos.session, &state.cache, &auth.did)
|
||||
.await
|
||||
.log_db_err("updating reauth")?;
|
||||
|
||||
@@ -127,7 +130,7 @@ pub async fn reauth_totp(
|
||||
)));
|
||||
}
|
||||
|
||||
let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did)
|
||||
let reauthed_at = update_last_reauth_cached(&*state.repos.session, &state.cache, &auth.did)
|
||||
.await
|
||||
.log_db_err("updating reauth")?;
|
||||
|
||||
@@ -146,7 +149,8 @@ pub async fn reauth_passkey_start(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<PasskeyReauthStartOutput>, ApiError> {
|
||||
let stored_passkeys = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_passkeys_for_user(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting passkeys")?;
|
||||
@@ -179,7 +183,8 @@ pub async fn reauth_passkey_start(
|
||||
})?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.save_webauthn_challenge(
|
||||
&auth.did,
|
||||
WebauthnChallengeType::Authentication,
|
||||
@@ -204,7 +209,8 @@ pub async fn reauth_passkey_finish(
|
||||
Json(input): Json<PasskeyReauthFinishInput>,
|
||||
) -> Result<Json<ReauthOutput>, ApiError> {
|
||||
let auth_state_json = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.load_webauthn_challenge(&auth.did, WebauthnChallengeType::Authentication)
|
||||
.await
|
||||
.log_db_err("loading authentication state")?
|
||||
@@ -232,7 +238,8 @@ pub async fn reauth_passkey_finish(
|
||||
|
||||
let cred_id_bytes = auth_result.cred_id().as_ref();
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_passkey_counter(
|
||||
cred_id_bytes,
|
||||
i32::try_from(auth_result.counter()).unwrap_or(i32::MAX),
|
||||
@@ -242,7 +249,8 @@ pub async fn reauth_passkey_finish(
|
||||
Ok(false) => {
|
||||
warn!(did = %&auth.did, "Passkey counter anomaly detected - possible cloned key");
|
||||
let _ = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.delete_webauthn_challenge(&auth.did, WebauthnChallengeType::Authentication)
|
||||
.await;
|
||||
return Err(ApiError::PasskeyCounterAnomaly);
|
||||
@@ -254,11 +262,12 @@ pub async fn reauth_passkey_finish(
|
||||
}
|
||||
|
||||
let _ = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.delete_webauthn_challenge(&auth.did, WebauthnChallengeType::Authentication)
|
||||
.await;
|
||||
|
||||
let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did)
|
||||
let reauthed_at = update_last_reauth_cached(&*state.repos.session, &state.cache, &auth.did)
|
||||
.await
|
||||
.log_db_err("updating reauth")?;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ pub async fn get_service_auth(
|
||||
Some(kb) => kb.clone(),
|
||||
None => {
|
||||
warn!(did = %&auth.did, "getServiceAuth: no key_bytes in auth, fetching from DB");
|
||||
match state.user_repo.get_user_info_by_did(&auth.did).await {
|
||||
match state.repos.user.get_user_info_by_did(&auth.did).await {
|
||||
Ok(Some(info)) => match info.key_bytes {
|
||||
Some(key_bytes_enc) => {
|
||||
match tranquil_pds::config::decrypt_key(
|
||||
@@ -112,7 +112,6 @@ pub async fn get_service_auth(
|
||||
};
|
||||
|
||||
let lxm = params.lxm.as_ref();
|
||||
let lxm_for_token = lxm.map_or("*", |v| v.as_str());
|
||||
|
||||
if let Some(method) = lxm {
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_rpc_scope(
|
||||
@@ -173,7 +172,7 @@ pub async fn get_service_auth(
|
||||
let service_token = match tranquil_pds::auth::create_service_token(
|
||||
&auth.did,
|
||||
params.aud.as_str(),
|
||||
lxm_for_token,
|
||||
lxm.map(|v| v.as_str()),
|
||||
&key_bytes,
|
||||
) {
|
||||
Ok(t) => t,
|
||||
|
||||
@@ -69,7 +69,8 @@ pub async fn create_session(
|
||||
input.identifier, normalized_identifier
|
||||
);
|
||||
let row = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_login_full_by_identifier(normalized_identifier.as_str())
|
||||
.await
|
||||
{
|
||||
@@ -98,7 +99,7 @@ pub async fn create_session(
|
||||
}
|
||||
};
|
||||
let credential = crate::common::verify_credential(
|
||||
state.session_repo.as_ref(),
|
||||
state.repos.session.as_ref(),
|
||||
row.id,
|
||||
&input.password,
|
||||
row.password_hash.as_deref(),
|
||||
@@ -130,7 +131,8 @@ pub async fn create_session(
|
||||
}
|
||||
let is_verified = row.channel_verification.has_any_verified();
|
||||
let is_delegated = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.is_delegated_account(&row.did)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
@@ -181,8 +183,8 @@ pub async fn create_session(
|
||||
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_2fa_code(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
row.id,
|
||||
code.as_str(),
|
||||
hostname,
|
||||
@@ -269,8 +271,8 @@ pub async fn create_session(
|
||||
app_password_name: app_password_name.clone(),
|
||||
};
|
||||
let (insert_result, did_doc) = tokio::join!(
|
||||
state.session_repo.create_session(&session_data),
|
||||
did_resolver.resolve_did_document(&did_for_doc)
|
||||
state.repos.session.create_session(&session_data),
|
||||
did_resolver.fetch_did_document(&did_for_doc),
|
||||
);
|
||||
if let Err(e) = insert_result {
|
||||
error!("Failed to insert session: {:?}", e);
|
||||
@@ -284,8 +286,8 @@ pub async fn create_session(
|
||||
);
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_legacy_login(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
row.id,
|
||||
hostname,
|
||||
client_ip,
|
||||
@@ -311,7 +313,7 @@ pub async fn create_session(
|
||||
refresh_jwt: refresh_meta.token,
|
||||
handle,
|
||||
did: row.did,
|
||||
did_doc,
|
||||
did_doc: did_doc.ok().map(|f| (*f).clone()),
|
||||
email: row.email,
|
||||
email_confirmed: Some(row.channel_verification.email),
|
||||
email_auth_factor: email_auth_factor_out,
|
||||
@@ -359,8 +361,8 @@ pub async fn get_session(
|
||||
let did_for_doc = auth.did.clone();
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
let (db_result, did_doc) = tokio::join!(
|
||||
state.user_repo.get_session_info_by_did(&auth.did),
|
||||
did_resolver.resolve_did_document(&did_for_doc)
|
||||
state.repos.user.get_session_info_by_did(&auth.did),
|
||||
did_resolver.fetch_did_document(&did_for_doc)
|
||||
);
|
||||
match db_result {
|
||||
Ok(Some(row)) => {
|
||||
@@ -404,7 +406,7 @@ pub async fn get_session(
|
||||
status: account_state.status_for_session().map(String::from),
|
||||
migrated_to_pds,
|
||||
migrated_at,
|
||||
did_doc,
|
||||
did_doc: did_doc.ok().map(|f| (*f).clone()),
|
||||
}))
|
||||
}
|
||||
Ok(None) => Err(ApiError::AuthenticationFailed(None)),
|
||||
@@ -422,7 +424,7 @@ pub async fn delete_session(
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
let jti = tranquil_pds::auth::extract_jti_from_headers(&headers)
|
||||
.ok_or(ApiError::AuthenticationRequired)?;
|
||||
match state.session_repo.delete_session_by_access_jti(&jti).await {
|
||||
match state.repos.session.delete_session_by_access_jti(&jti).await {
|
||||
Ok(rows) if rows > 0 => {
|
||||
let session_cache_key = tranquil_pds::cache_keys::session_key(&auth.did, &jti);
|
||||
let _ = state.cache.delete(&session_cache_key).await;
|
||||
@@ -476,7 +478,8 @@ pub async fn refresh_session(
|
||||
}
|
||||
};
|
||||
if let Ok(Some(_)) = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.check_refresh_token_used(&refresh_jti)
|
||||
.await
|
||||
{
|
||||
@@ -486,7 +489,8 @@ pub async fn refresh_session(
|
||||
)));
|
||||
}
|
||||
let session_row = match state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.get_session_for_refresh(&refresh_jti)
|
||||
.await
|
||||
{
|
||||
@@ -548,7 +552,8 @@ pub async fn refresh_session(
|
||||
new_refresh_expires_at: new_refresh_meta.expires_at,
|
||||
};
|
||||
match state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.refresh_session_atomic(&refresh_data)
|
||||
.await
|
||||
{
|
||||
@@ -576,8 +581,8 @@ pub async fn refresh_session(
|
||||
let did_for_doc = session_row.did.clone();
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
let (db_result, did_doc) = tokio::join!(
|
||||
state.user_repo.get_session_info_by_did(&session_row.did),
|
||||
did_resolver.resolve_did_document(&did_for_doc)
|
||||
state.repos.user.get_session_info_by_did(&session_row.did),
|
||||
did_resolver.fetch_did_document(&did_for_doc)
|
||||
);
|
||||
match db_result {
|
||||
Ok(Some(u)) => {
|
||||
@@ -599,7 +604,7 @@ pub async fn refresh_session(
|
||||
preferred_locale: u.preferred_locale,
|
||||
is_admin: u.is_admin,
|
||||
active: account_state.is_active(),
|
||||
did_doc,
|
||||
did_doc: did_doc.ok().map(|f| (*f).clone()),
|
||||
status: account_state.status_for_session().map(String::from),
|
||||
}))
|
||||
}
|
||||
@@ -639,7 +644,7 @@ pub async fn confirm_signup(
|
||||
Json(input): Json<ConfirmSignupInput>,
|
||||
) -> Result<Json<ConfirmSignupOutput>, ApiError> {
|
||||
info!("confirm_signup called for DID: {}", input.did);
|
||||
let row = match state.user_repo.get_confirm_signup_by_did(&input.did).await {
|
||||
let row = match state.repos.user.get_confirm_signup_by_did(&input.did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
warn!("User not found for confirm_signup: {}", input.did);
|
||||
@@ -702,7 +707,8 @@ pub async fn confirm_signup(
|
||||
};
|
||||
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_channel_verified(&input.did, row.channel)
|
||||
.await
|
||||
{
|
||||
@@ -726,8 +732,8 @@ pub async fn confirm_signup(
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
row.id,
|
||||
hostname,
|
||||
)
|
||||
@@ -757,7 +763,7 @@ pub struct AutoResendResult {
|
||||
pub async fn auto_resend_verification(state: &AppState, did: &Did) -> Option<AutoResendResult> {
|
||||
let debounce_key = tranquil_pds::cache_keys::auto_verify_sent_key(did.as_str());
|
||||
let debounced = state.cache.get(&debounce_key).await.is_some();
|
||||
let row = match state.user_repo.get_resend_verification_by_did(did).await {
|
||||
let row = match state.repos.user.get_resend_verification_by_did(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return None,
|
||||
Err(e) => {
|
||||
@@ -821,7 +827,8 @@ pub async fn resend_verification(
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
info!("resend_verification called for DID: {}", input.did);
|
||||
let row = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_resend_verification_by_did(&input.did)
|
||||
.await
|
||||
{
|
||||
@@ -895,13 +902,15 @@ pub async fn list_sessions(
|
||||
let current_jti = tranquil_pds::auth::extract_jti_from_headers(&headers);
|
||||
|
||||
let jwt_rows = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.list_sessions_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching JWT sessions")?;
|
||||
|
||||
let oauth_rows = state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.list_sessions_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching OAuth sessions")?;
|
||||
@@ -962,13 +971,15 @@ pub async fn revoke_session(
|
||||
.map(SessionId::new)
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?;
|
||||
let access_jti = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.get_session_access_jti_by_id(session_id, &auth.did)
|
||||
.await
|
||||
.log_db_err("in revoke_session")?
|
||||
.ok_or(ApiError::SessionNotFound)?;
|
||||
state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.delete_session_by_id(session_id)
|
||||
.await
|
||||
.log_db_err("deleting session")?;
|
||||
@@ -983,7 +994,8 @@ pub async fn revoke_session(
|
||||
.map(TokenFamilyId::new)
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?;
|
||||
let deleted = state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.delete_session_by_id(session_id, &auth.did)
|
||||
.await
|
||||
.log_db_err("deleting OAuth session")?;
|
||||
@@ -1007,24 +1019,28 @@ pub async fn revoke_all_sessions(
|
||||
|
||||
if auth.is_oauth() {
|
||||
state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.delete_sessions_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("revoking JWT sessions")?;
|
||||
let jti_typed = TokenId::from(jti.clone());
|
||||
state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.delete_sessions_by_did_except(&auth.did, &jti_typed)
|
||||
.await
|
||||
.log_db_err("revoking OAuth sessions")?;
|
||||
} else {
|
||||
state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.delete_sessions_by_did_except_jti(&auth.did, &jti)
|
||||
.await
|
||||
.log_db_err("revoking JWT sessions")?;
|
||||
state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.delete_sessions_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("revoking OAuth sessions")?;
|
||||
@@ -1046,7 +1062,8 @@ pub async fn get_legacy_login_preference(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<LegacyLoginPreferenceOutput>, ApiError> {
|
||||
let pref = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_legacy_login_pref(&auth.did)
|
||||
.await
|
||||
.log_db_err("getting legacy login pref")?
|
||||
@@ -1079,7 +1096,8 @@ pub async fn update_legacy_login_preference(
|
||||
let reauth_mfa = require_reauth_window(&state, &auth).await?;
|
||||
|
||||
let updated = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_legacy_login(reauth_mfa.did(), input.allow_legacy_login)
|
||||
.await
|
||||
.log_db_err("updating legacy login")?;
|
||||
@@ -1117,7 +1135,8 @@ pub async fn update_locale(
|
||||
}
|
||||
|
||||
let updated = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_locale(&auth.did, &input.preferred_locale)
|
||||
.await
|
||||
.log_db_err("updating locale")?;
|
||||
|
||||
@@ -44,7 +44,8 @@ pub async fn reserve_signing_key(
|
||||
let expires_at = Utc::now() + Duration::hours(24);
|
||||
let private_bytes: &[u8] = &private_key_bytes;
|
||||
match state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.reserve_signing_key(
|
||||
input.did.as_ref(),
|
||||
&public_key_did_key,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub async fn create_totp_secret(
|
||||
) -> Result<Json<CreateTotpSecretOutput>, ApiError> {
|
||||
use tranquil_db_traits::TotpRecordState;
|
||||
|
||||
match state.user_repo.get_totp_record_state(&auth.did).await {
|
||||
match state.repos.user.get_totp_record_state(&auth.did).await {
|
||||
Ok(Some(TotpRecordState::Verified(_))) => return Err(ApiError::TotpAlreadyEnabled),
|
||||
Ok(Some(TotpRecordState::Unverified(_))) | Ok(None) => {}
|
||||
Err(e) => {
|
||||
@@ -41,7 +41,8 @@ pub async fn create_totp_secret(
|
||||
let secret = generate_totp_secret();
|
||||
|
||||
let handle = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_handle_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching handle")?
|
||||
@@ -61,7 +62,8 @@ pub async fn create_totp_secret(
|
||||
})?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION)
|
||||
.await
|
||||
.log_db_err("storing TOTP secret")?;
|
||||
@@ -102,7 +104,7 @@ pub async fn enable_totp(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let unverified_record = match state.user_repo.get_totp_record_state(&auth.did).await {
|
||||
let unverified_record = match state.repos.user.get_totp_record_state(&auth.did).await {
|
||||
Ok(Some(TotpRecordState::Unverified(record))) => record,
|
||||
Ok(Some(TotpRecordState::Verified(_))) => return Err(ApiError::TotpAlreadyEnabled),
|
||||
Ok(None) => return Err(ApiError::TotpNotEnabled),
|
||||
@@ -139,7 +141,8 @@ pub async fn enable_totp(
|
||||
})?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.enable_totp_with_backup_codes(&auth.did, &backup_hashes)
|
||||
.await
|
||||
.log_db_err("enabling TOTP")?;
|
||||
@@ -173,7 +176,8 @@ pub async fn disable_totp(
|
||||
let totp_mfa = verify_totp_mfa(&state, &auth, &input.code).await?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.delete_totp_and_backup_codes(totp_mfa.did())
|
||||
.await
|
||||
.log_db_err("deleting TOTP")?;
|
||||
@@ -199,7 +203,7 @@ pub async fn get_totp_status(
|
||||
) -> Result<Json<GetTotpStatusOutput>, ApiError> {
|
||||
use tranquil_db_traits::TotpRecordState;
|
||||
|
||||
let enabled = match state.user_repo.get_totp_record_state(&auth.did).await {
|
||||
let enabled = match state.repos.user.get_totp_record_state(&auth.did).await {
|
||||
Ok(Some(TotpRecordState::Verified(_))) => true,
|
||||
Ok(Some(TotpRecordState::Unverified(_))) | Ok(None) => false,
|
||||
Err(e) => {
|
||||
@@ -209,7 +213,8 @@ pub async fn get_totp_status(
|
||||
};
|
||||
|
||||
let backup_count = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.count_unused_backup_codes(&auth.did)
|
||||
.await
|
||||
.log_db_err("counting backup codes")?;
|
||||
@@ -259,7 +264,8 @@ pub async fn regenerate_backup_codes(
|
||||
})?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.replace_backup_codes(totp_mfa.did(), &backup_hashes)
|
||||
.await
|
||||
.log_db_err("replacing backup codes")?;
|
||||
@@ -276,7 +282,7 @@ async fn verify_backup_code_for_user(
|
||||
) -> bool {
|
||||
let code = code.trim().to_uppercase();
|
||||
|
||||
let backup_codes = match state.user_repo.get_unused_backup_codes(did).await {
|
||||
let backup_codes = match state.repos.user.get_unused_backup_codes(did).await {
|
||||
Ok(codes) => codes,
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch backup codes: {:?}", e);
|
||||
@@ -290,7 +296,7 @@ async fn verify_backup_code_for_user(
|
||||
|
||||
match matched {
|
||||
Some(row) => {
|
||||
let _ = state.user_repo.mark_backup_code_used(row.id).await;
|
||||
let _ = state.repos.user.mark_backup_code_used(row.id).await;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
@@ -310,7 +316,7 @@ pub async fn verify_totp_or_backup_for_user(
|
||||
return verify_backup_code_for_user(state, did, code).await;
|
||||
}
|
||||
|
||||
let verified_record = match state.user_repo.get_totp_record_state(did).await {
|
||||
let verified_record = match state.repos.user.get_totp_record_state(did).await {
|
||||
Ok(Some(TotpRecordState::Verified(record))) => record,
|
||||
_ => return false,
|
||||
};
|
||||
@@ -324,7 +330,7 @@ pub async fn verify_totp_or_backup_for_user(
|
||||
};
|
||||
|
||||
if verify_totp_code(&secret, code) {
|
||||
let _ = state.user_repo.update_totp_last_used(did).await;
|
||||
let _ = state.repos.user.update_totp_last_used(did).await;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -332,5 +338,10 @@ pub async fn verify_totp_or_backup_for_user(
|
||||
}
|
||||
|
||||
pub async fn has_totp_enabled(state: &AppState, did: &tranquil_pds::types::Did) -> bool {
|
||||
state.user_repo.has_totp_enabled(did).await.unwrap_or(false)
|
||||
state
|
||||
.repos
|
||||
.user
|
||||
.has_totp_enabled(did)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,8 @@ pub async fn list_trusted_devices(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<ListTrustedDevicesOutput>, ApiError> {
|
||||
let rows = state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.list_trusted_devices(&auth.did)
|
||||
.await
|
||||
.log_db_err("listing trusted devices")?;
|
||||
@@ -108,7 +109,8 @@ pub async fn revoke_trusted_device(
|
||||
Json(input): Json<RevokeTrustedDeviceInput>,
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.device_belongs_to_user(&input.device_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
@@ -123,8 +125,9 @@ pub async fn revoke_trusted_device(
|
||||
}
|
||||
|
||||
state
|
||||
.oauth_repo
|
||||
.revoke_device_trust(&input.device_id)
|
||||
.repos
|
||||
.oauth
|
||||
.revoke_device_trust(&input.device_id, &auth.did)
|
||||
.await
|
||||
.log_db_err("revoking device trust")?;
|
||||
|
||||
@@ -145,7 +148,8 @@ pub async fn update_trusted_device(
|
||||
Json(input): Json<UpdateTrustedDeviceInput>,
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.device_belongs_to_user(&input.device_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
@@ -160,8 +164,9 @@ pub async fn update_trusted_device(
|
||||
}
|
||||
|
||||
state
|
||||
.oauth_repo
|
||||
.update_device_friendly_name(&input.device_id, input.friendly_name.as_deref())
|
||||
.repos
|
||||
.oauth
|
||||
.update_device_friendly_name(&input.device_id, &auth.did, input.friendly_name.as_deref())
|
||||
.await
|
||||
.log_db_err("updating device friendly name")?;
|
||||
|
||||
@@ -193,18 +198,22 @@ pub async fn is_device_trusted(
|
||||
pub async fn trust_device(
|
||||
oauth_repo: &dyn OAuthRepository,
|
||||
device_id: &DeviceId,
|
||||
did: &tranquil_types::Did,
|
||||
) -> Result<(), tranquil_db_traits::DbError> {
|
||||
let now = Utc::now();
|
||||
let trusted_until = now + Duration::days(TRUST_DURATION_DAYS);
|
||||
oauth_repo.trust_device(device_id, now, trusted_until).await
|
||||
oauth_repo
|
||||
.trust_device(device_id, did, now, trusted_until)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn extend_device_trust(
|
||||
oauth_repo: &dyn OAuthRepository,
|
||||
device_id: &DeviceId,
|
||||
did: &tranquil_types::Did,
|
||||
) -> Result<(), tranquil_db_traits::DbError> {
|
||||
let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS);
|
||||
oauth_repo
|
||||
.extend_device_trust(device_id, trusted_until)
|
||||
.extend_device_trust(device_id, did, trusted_until)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ pub async fn resend_migration_verification(
|
||||
.unwrap_or(tranquil_db_traits::CommsChannel::Email);
|
||||
let identifier = input.identifier.trim().to_lowercase();
|
||||
|
||||
let user = match state.user_repo.get_by_email(&identifier).await {
|
||||
let user = match state.repos.user.get_by_email(&identifier).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return Ok(Json(ResendMigrationVerificationOutput { sent: true }));
|
||||
|
||||
@@ -79,7 +79,8 @@ async fn handle_migration_verification(
|
||||
identifier: &str,
|
||||
) -> Result<Json<VerifyTokenOutput>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_verification_info(did)
|
||||
.await
|
||||
.log_db_err("during migration verification")?
|
||||
@@ -92,13 +93,14 @@ async fn handle_migration_verification(
|
||||
}
|
||||
if !user.channel_verification.email {
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.set_email_verified_flag(user.id)
|
||||
.await
|
||||
.log_db_err("updating email_verified status")?;
|
||||
}
|
||||
}
|
||||
_ => common::set_channel_verified_flag(state.user_repo.as_ref(), user.id, channel).await?,
|
||||
_ => common::set_channel_verified_flag(state.repos.user.as_ref(), user.id, channel).await?,
|
||||
};
|
||||
|
||||
info!(did = %did, channel = ?channel, "Migration verification completed successfully");
|
||||
@@ -118,7 +120,8 @@ async fn handle_channel_update(
|
||||
identifier: &str,
|
||||
) -> Result<Json<VerifyTokenOutput>, ApiError> {
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.log_db_err("fetching user id")?
|
||||
@@ -127,7 +130,8 @@ async fn handle_channel_update(
|
||||
match channel {
|
||||
CommsChannel::Email => {
|
||||
let success = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.verify_email_channel(user_id, identifier)
|
||||
.await
|
||||
.log_db_err("updating email channel")?;
|
||||
@@ -137,21 +141,24 @@ async fn handle_channel_update(
|
||||
}
|
||||
CommsChannel::Discord => {
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.verify_discord_channel(user_id, identifier)
|
||||
.await
|
||||
.log_db_err("updating discord channel")?;
|
||||
}
|
||||
CommsChannel::Telegram => {
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.verify_telegram_channel(user_id, identifier)
|
||||
.await
|
||||
.log_db_err("updating telegram channel")?;
|
||||
}
|
||||
CommsChannel::Signal => {
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.verify_signal_channel(user_id, identifier)
|
||||
.await
|
||||
.log_db_err("updating signal channel")?;
|
||||
@@ -160,19 +167,7 @@ async fn handle_channel_update(
|
||||
|
||||
info!(did = %did, channel = ?channel, "Channel verified successfully");
|
||||
|
||||
let recipient = resolve_verified_recipient(state, user_id, channel, identifier).await;
|
||||
if let Err(e) = comms_repo::enqueue_channel_verified(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
channel,
|
||||
&recipient,
|
||||
&tranquil_config::get().server.hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(error = %e, "Failed to enqueue channel verified notification");
|
||||
}
|
||||
notify_channel_verified(state, user_id, channel, identifier).await;
|
||||
|
||||
Ok(Json(VerifyTokenOutput {
|
||||
success: true,
|
||||
@@ -182,15 +177,16 @@ async fn handle_channel_update(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn resolve_verified_recipient(
|
||||
async fn notify_channel_verified(
|
||||
state: &AppState,
|
||||
user_id: uuid::Uuid,
|
||||
channel: tranquil_db_traits::CommsChannel,
|
||||
channel: CommsChannel,
|
||||
identifier: &str,
|
||||
) -> String {
|
||||
match channel {
|
||||
tranquil_db_traits::CommsChannel::Telegram => state
|
||||
.user_repo
|
||||
) {
|
||||
let recipient = match channel {
|
||||
CommsChannel::Telegram => state
|
||||
.repos
|
||||
.user
|
||||
.get_telegram_chat_id(user_id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -198,6 +194,18 @@ async fn resolve_verified_recipient(
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_else(|| identifier.to_string()),
|
||||
_ => identifier.to_string(),
|
||||
};
|
||||
if let Err(e) = comms_repo::enqueue_channel_verified(
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
channel,
|
||||
&recipient,
|
||||
&tranquil_config::get().server.hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(error = %e, "Failed to enqueue channel verified notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +216,8 @@ async fn handle_signup_verification(
|
||||
identifier: &str,
|
||||
) -> Result<Json<VerifyTokenOutput>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_verification_info(did)
|
||||
.await
|
||||
.log_db_err("during signup verification")?
|
||||
@@ -225,23 +234,11 @@ async fn handle_signup_verification(
|
||||
}));
|
||||
}
|
||||
|
||||
common::set_channel_verified_flag(state.user_repo.as_ref(), user.id, channel).await?;
|
||||
common::set_channel_verified_flag(state.repos.user.as_ref(), user.id, channel).await?;
|
||||
|
||||
info!(did = %did, channel = ?channel, "Signup verified successfully");
|
||||
|
||||
let recipient = resolve_verified_recipient(state, user.id, channel, identifier).await;
|
||||
if let Err(e) = comms_repo::enqueue_channel_verified(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user.id,
|
||||
channel,
|
||||
&recipient,
|
||||
&tranquil_config::get().server.hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(error = %e, "Failed to enqueue channel verified notification");
|
||||
}
|
||||
notify_channel_verified(state, user.id, channel, identifier).await;
|
||||
|
||||
Ok(Json(VerifyTokenOutput {
|
||||
success: true,
|
||||
|
||||
@@ -71,7 +71,8 @@ pub async fn handle_telegram_webhook(
|
||||
"Received /start from Telegram user"
|
||||
);
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.store_telegram_chat_id(&username, from.id, handle.as_deref())
|
||||
.await
|
||||
{
|
||||
@@ -82,8 +83,8 @@ pub async fn handle_telegram_webhook(
|
||||
"Verified Telegram user and stored chat_id"
|
||||
);
|
||||
if let Err(e) = comms_repo::enqueue_channel_verified(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
tranquil_db_traits::CommsChannel::Telegram,
|
||||
&from.id.to_string(),
|
||||
|
||||
@@ -74,7 +74,12 @@ pub fn create_refresh_token_with_metadata(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> {
|
||||
pub fn create_service_token(
|
||||
did: &str,
|
||||
aud: &str,
|
||||
lxm: Option<&str>,
|
||||
key_bytes: &[u8],
|
||||
) -> Result<String> {
|
||||
let signing_key = SigningKey::from_slice(key_bytes)?;
|
||||
|
||||
let expiration = Utc::now()
|
||||
@@ -89,7 +94,7 @@ pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -
|
||||
exp: expiration,
|
||||
iat: Utc::now().timestamp(),
|
||||
scope: None,
|
||||
lxm: Some(lxm.to_string()),
|
||||
lxm: lxm.map(ToOwned::to_owned),
|
||||
jti: uuid::Uuid::new_v4().to_string(),
|
||||
act: None,
|
||||
};
|
||||
|
||||
@@ -6,12 +6,15 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-signal = { workspace = true }
|
||||
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tranquil-db-traits = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::types::{CommsChannel, QueuedComms};
|
||||
|
||||
@@ -58,6 +57,49 @@ async fn retry_delay(attempt: u32) {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
|
||||
async fn send_http_with_retry<F, Fut>(service_name: &str, send_request: F) -> Result<(), SendError>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: std::future::Future<Output = Result<reqwest::Response, reqwest::Error>>,
|
||||
{
|
||||
let mut last_error = None;
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
match send_request().await {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
let status = response.status();
|
||||
if is_retryable_status(status) && attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some(format!("{service_name} API returned {status}"));
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"{service_name} API returned {status}: {body}",
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_timeout() {
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some(format!("{service_name} request timed out"));
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
return Err(SendError::Timeout);
|
||||
}
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"{service_name} request failed: {e}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(SendError::MaxRetriesExceeded(
|
||||
last_error.unwrap_or_else(|| "unknown error".to_string()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn sanitize_header_value(value: &str) -> String {
|
||||
value.replace(['\r', '\n'], " ").trim().to_string()
|
||||
}
|
||||
@@ -90,22 +132,7 @@ pub fn is_valid_phone_number(number: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn is_valid_signal_username(username: &str) -> bool {
|
||||
if username.len() < 6 || username.len() > 35 {
|
||||
return false;
|
||||
}
|
||||
let Some((base, discriminator)) = username.rsplit_once('.') else {
|
||||
return false;
|
||||
};
|
||||
if base.len() < 3 || base.len() > 32 {
|
||||
return false;
|
||||
}
|
||||
if !base.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
||||
return false;
|
||||
}
|
||||
if !base.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
|
||||
return false;
|
||||
}
|
||||
discriminator.len() == 2 && discriminator.chars().all(|c| c.is_ascii_digit())
|
||||
tranquil_signal::SignalUsername::parse(username).is_ok()
|
||||
}
|
||||
|
||||
pub struct EmailSender {
|
||||
@@ -414,51 +441,14 @@ impl CommsSender for DiscordSender {
|
||||
let payload = json!({ "content": content });
|
||||
let url = format!("{}/channels/{}/messages", DISCORD_API_BASE, channel_id);
|
||||
|
||||
let mut last_error = None;
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
let result = self
|
||||
.http_client
|
||||
send_http_with_retry("Discord", || {
|
||||
self.http_client
|
||||
.post(&url)
|
||||
.header("Authorization", self.auth_header())
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
let status = response.status();
|
||||
if is_retryable_status(status) && attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some(format!("Discord API returned {}", status));
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"Discord API returned {}: {}",
|
||||
status, body
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_timeout() {
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some("Discord request timed out".to_string());
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
return Err(SendError::Timeout);
|
||||
}
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"Discord request failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(SendError::MaxRetriesExceeded(
|
||||
last_error.unwrap_or_else(|| "Unknown error".to_string()),
|
||||
))
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,79 +542,22 @@ impl CommsSender for TelegramSender {
|
||||
"text": text,
|
||||
"parse_mode": "HTML"
|
||||
});
|
||||
let mut last_error = None;
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
let result = self.http_client.post(&url).json(&payload).send().await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
let status = response.status();
|
||||
if is_retryable_status(status) && attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some(format!("Telegram API returned {}", status));
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"Telegram API returned {}: {}",
|
||||
status, body
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_timeout() {
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some("Telegram request timed out".to_string());
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
return Err(SendError::Timeout);
|
||||
}
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"Telegram request failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(SendError::MaxRetriesExceeded(
|
||||
last_error.unwrap_or_else(|| "Unknown error".to_string()),
|
||||
))
|
||||
|
||||
send_http_with_retry("Telegram", || {
|
||||
self.http_client.post(&url).json(&payload).send()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SignalSender {
|
||||
signal_cli_path: String,
|
||||
sender_number: String,
|
||||
slot: std::sync::Arc<tranquil_signal::SignalSlot>,
|
||||
}
|
||||
|
||||
impl SignalSender {
|
||||
pub fn new(signal_cli_path: String, sender_number: String) -> Self {
|
||||
Self {
|
||||
signal_cli_path,
|
||||
sender_number,
|
||||
}
|
||||
pub fn new(slot: std::sync::Arc<tranquil_signal::SignalSlot>) -> Self {
|
||||
Self { slot }
|
||||
}
|
||||
|
||||
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
|
||||
let signal_cli_path = cfg.signal.cli_path.clone();
|
||||
let sender_number = cfg.signal.sender_number.clone()?;
|
||||
Some(Self::new(signal_cli_path, sender_number))
|
||||
}
|
||||
}
|
||||
|
||||
const SIGNAL_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
fn is_retryable_signal_error(stderr: &str) -> bool {
|
||||
let lower = stderr.to_lowercase();
|
||||
lower.contains("timeout")
|
||||
|| lower.contains("timed out")
|
||||
|| lower.contains("connection refused")
|
||||
|| lower.contains("network")
|
||||
|| lower.contains("temporarily")
|
||||
|| lower.contains("try again")
|
||||
|| lower.contains("rate limit")
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -634,67 +567,46 @@ impl CommsSender for SignalSender {
|
||||
}
|
||||
|
||||
async fn send(&self, notification: &QueuedComms) -> Result<(), SendError> {
|
||||
let recipient = ¬ification.recipient;
|
||||
if !is_valid_signal_username(recipient) {
|
||||
return Err(SendError::InvalidRecipient(format!(
|
||||
"Invalid Signal username format: {}",
|
||||
recipient
|
||||
)));
|
||||
}
|
||||
let username = tranquil_signal::SignalUsername::parse(¬ification.recipient)
|
||||
.map_err(|e| SendError::InvalidRecipient(e.to_string()))?;
|
||||
|
||||
let client = self
|
||||
.slot
|
||||
.client()
|
||||
.await
|
||||
.ok_or(SendError::NotConfigured(CommsChannel::Signal))?;
|
||||
|
||||
let subject = notification.subject.as_deref().unwrap_or("Notification");
|
||||
let message = format!("{}\n\n{}", subject, notification.body);
|
||||
let raw_message = format!("{}\n\n{}", subject, notification.body);
|
||||
let message = tranquil_signal::MessageBody::new(raw_message)
|
||||
.map_err(|e| SendError::InvalidRecipient(e.to_string()))?;
|
||||
|
||||
let mut last_error = None;
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
let cmd_future = Command::new(&self.signal_cli_path)
|
||||
.arg("-u")
|
||||
.arg(&self.sender_number)
|
||||
.arg("send")
|
||||
.arg("--username")
|
||||
.arg(recipient)
|
||||
.arg("-m")
|
||||
.arg(&message)
|
||||
.output();
|
||||
|
||||
let result = timeout(Duration::from_secs(SIGNAL_TIMEOUT_SECS), cmd_future).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(output)) if output.status.success() => return Ok(()),
|
||||
Ok(Ok(output)) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if is_retryable_signal_error(&stderr) && attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some(format!("signal-cli failed: {}", stderr));
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
match client.send(&username, message.clone()).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
match &e {
|
||||
tranquil_signal::SignalError::UsernameNotFound(_)
|
||||
| tranquil_signal::SignalError::UsernameLookup(_)
|
||||
| tranquil_signal::SignalError::NotLinked => {
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"signal send failed: {err_str}"
|
||||
)));
|
||||
}
|
||||
_ => {
|
||||
last_error = Some(err_str);
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
retry_delay(attempt).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(SendError::ExternalService(format!(
|
||||
"signal-cli failed: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some(format!("signal-cli spawn failed: {}", e));
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
return Err(SendError::ProcessSpawn {
|
||||
command: self.signal_cli_path.clone(),
|
||||
source: e,
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
last_error = Some("signal-cli timed out".to_string());
|
||||
retry_delay(attempt).await;
|
||||
continue;
|
||||
}
|
||||
return Err(SendError::Timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(SendError::MaxRetriesExceeded(
|
||||
last_error.unwrap_or_else(|| "Unknown error".to_string()),
|
||||
last_error.unwrap_or_else(|| "unknown error".to_string()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,9 @@ pub struct TranquilConfig {
|
||||
|
||||
#[config(nested)]
|
||||
pub scheduled: ScheduledConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub tranquil_store: TranquilStoreConfig,
|
||||
}
|
||||
|
||||
impl TranquilConfig {
|
||||
@@ -250,6 +253,55 @@ impl TranquilConfig {
|
||||
);
|
||||
}
|
||||
|
||||
// -- repo backend -----------------------------------------------------
|
||||
if let Err(e) = self.storage.repo_backend.parse::<RepoBackend>() {
|
||||
errors.push(e);
|
||||
}
|
||||
|
||||
// -- tranquil-store ---------------------------------------------------
|
||||
if let Some(mb) = self.tranquil_store.memory_budget_mb
|
||||
&& mb == 0
|
||||
{
|
||||
errors.push("tranquil_store.memory_budget_mb must be at least 1".to_string());
|
||||
}
|
||||
if let Some(threads) = self.tranquil_store.handler_threads
|
||||
&& threads == 0
|
||||
{
|
||||
errors.push("tranquil_store.handler_threads must be at least 1".to_string());
|
||||
}
|
||||
if self.tranquil_store.eventlog_max_event_payload == 0 {
|
||||
errors.push(
|
||||
"tranquil_store.eventlog_max_event_payload \
|
||||
(TRANQUIL_STORE_EVENTLOG_MAX_EVENT_PAYLOAD) must be at least 1; \
|
||||
a value of 0 would reject every event"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// -- scheduled / event retention --------------------------------------
|
||||
const MAX_RETENTION_SECS: u64 = (i64::MAX / 1000) as u64;
|
||||
if self.scheduled.event_retention_max_age_secs > MAX_RETENTION_SECS {
|
||||
errors.push(format!(
|
||||
"scheduled.event_retention_max_age_secs (EVENT_RETENTION_MAX_AGE_SECS) \
|
||||
must be at most {MAX_RETENTION_SECS} (chrono::Duration limit); got {}",
|
||||
self.scheduled.event_retention_max_age_secs
|
||||
));
|
||||
}
|
||||
if self.scheduled.event_retention_interval_secs > 0 {
|
||||
let backfill_secs = u64::try_from(self.firehose.backfill_hours.max(0))
|
||||
.unwrap_or(0)
|
||||
.saturating_mul(3600);
|
||||
if self.scheduled.event_retention_max_age_secs < backfill_secs {
|
||||
errors.push(format!(
|
||||
"scheduled.event_retention_max_age_secs ({}) is shorter than \
|
||||
firehose.backfill_hours ({}h = {backfill_secs}s): \
|
||||
relays would receive cursor responses pointing at pruned events. \
|
||||
Increase event_retention_max_age_secs or decrease firehose.backfill_hours.",
|
||||
self.scheduled.event_retention_max_age_secs, self.firehose.backfill_hours,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// -- cache ------------------------------------------------------------
|
||||
match self.cache.backend.as_str() {
|
||||
"valkey" => {
|
||||
@@ -561,6 +613,35 @@ impl SecretsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RepoBackend {
|
||||
Postgres,
|
||||
TranquilStore,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for RepoBackend {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"postgres" => Ok(Self::Postgres),
|
||||
"tranquil-store" => Ok(Self::TranquilStore),
|
||||
other => Err(format!(
|
||||
"unknown repo backend \"{other}\", expected \"postgres\" or \"tranquil-store\""
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RepoBackend {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Postgres => f.write_str("postgres"),
|
||||
Self::TranquilStore => f.write_str("tranquil-store"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct StorageConfig {
|
||||
/// Storage backend: `filesystem` or `s3`.
|
||||
@@ -578,6 +659,17 @@ pub struct StorageConfig {
|
||||
/// Custom S3 endpoint URL (for MinIO, R2, etc.).
|
||||
#[config(env = "S3_ENDPOINT")]
|
||||
pub s3_endpoint: Option<String>,
|
||||
|
||||
#[config(env = "REPO_BACKEND", default = "postgres")]
|
||||
pub repo_backend: String,
|
||||
}
|
||||
|
||||
impl StorageConfig {
|
||||
pub fn repo_backend(&self) -> RepoBackend {
|
||||
self.repo_backend
|
||||
.parse()
|
||||
.expect("repo_backend must be validated before use")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
@@ -678,13 +770,8 @@ pub struct TelegramConfig {
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct SignalConfig {
|
||||
/// Path to the `signal-cli` binary.
|
||||
#[config(env = "SIGNAL_CLI_PATH", default = "/usr/local/bin/signal-cli")]
|
||||
pub cli_path: String,
|
||||
|
||||
/// Sender phone number. When unset, Signal integration is disabled.
|
||||
#[config(env = "SIGNAL_SENDER_NUMBER")]
|
||||
pub sender_number: Option<String>,
|
||||
#[config(env = "SIGNAL_ENABLED", default = false)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
@@ -1001,6 +1088,97 @@ pub struct ScheduledConfig {
|
||||
/// Interval in seconds between scheduled delete checks.
|
||||
#[config(env = "SCHEDULED_DELETE_CHECK_INTERVAL_SECS", default = 3600)]
|
||||
pub delete_check_interval_secs: u64,
|
||||
|
||||
/// Interval in seconds between data file compaction scans (tranquil-store only).
|
||||
/// Set to 0 to disable.
|
||||
#[config(env = "COMPACTION_INTERVAL_SECS", default = 3600)]
|
||||
pub compaction_interval_secs: u64,
|
||||
|
||||
/// Liveness ratio threshold below which a data file is compacted (0.0-1.0).
|
||||
#[config(env = "COMPACTION_LIVENESS_THRESHOLD", default = 0.7)]
|
||||
pub compaction_liveness_threshold: f64,
|
||||
|
||||
/// Grace period in milliseconds before a zero-refcount block can be removed by compaction.
|
||||
#[config(env = "COMPACTION_GRACE_PERIOD_MS", default = 600000)]
|
||||
pub compaction_grace_period_ms: u64,
|
||||
|
||||
/// Interval in seconds between reachability walk runs (tranquil-store only).
|
||||
/// Set to 0 to disable. Default: weekly.
|
||||
#[config(env = "REACHABILITY_WALK_INTERVAL_SECS", default = 604800)]
|
||||
pub reachability_walk_interval_secs: u64,
|
||||
|
||||
/// Interval in seconds between continuous archival passes (tranquil-store only).
|
||||
/// Sealed eventlog segments are copied to the archival destination each tick.
|
||||
/// Set to 0 to disable. Default: 60 seconds.
|
||||
#[config(env = "ARCHIVAL_INTERVAL_SECS", default = 60)]
|
||||
pub archival_interval_secs: u64,
|
||||
|
||||
/// Archival destination directory for sealed eventlog segments.
|
||||
/// If unset, archival is disabled.
|
||||
#[config(env = "ARCHIVAL_DEST_DIR")]
|
||||
pub archival_dest_dir: Option<String>,
|
||||
|
||||
/// Maximum age of events retained in the eventlog before pruning.
|
||||
/// Per the atproto firehose spec, the relay backfill window only needs
|
||||
/// to cover "hours or days". Default: 7 days.
|
||||
#[config(env = "EVENT_RETENTION_MAX_AGE_SECS", default = 604800)]
|
||||
pub event_retention_max_age_secs: u64,
|
||||
|
||||
/// Interval in seconds between event retention prune passes.
|
||||
/// Set to 0 to disable. Default: hourly.
|
||||
#[config(env = "EVENT_RETENTION_INTERVAL_SECS", default = 3600)]
|
||||
pub event_retention_interval_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct TranquilStoreConfig {
|
||||
/// Directory for tranquil-store data (metastore, eventlog).
|
||||
#[config(
|
||||
env = "TRANQUIL_STORE_DATA_DIR",
|
||||
default = "/var/lib/tranquil-pds/store"
|
||||
)]
|
||||
pub data_dir: String,
|
||||
|
||||
/// Fjall block cache size in megabytes. Defaults to 20% of system RAM
|
||||
/// when unset.
|
||||
#[config(env = "TRANQUIL_STORE_MEMORY_BUDGET_MB")]
|
||||
pub memory_budget_mb: Option<u64>,
|
||||
|
||||
/// Number of handler threads. Defaults to available_parallelism / 2.
|
||||
#[config(env = "TRANQUIL_STORE_HANDLER_THREADS")]
|
||||
pub handler_threads: Option<usize>,
|
||||
|
||||
/// Maximum total bytes of pending (unsynced) eventlog payloads. Appenders
|
||||
/// block once this budget is exhausted until in-flight events drain via
|
||||
/// fsync. Set to 0 to disable backpressure (unbounded). Default: 1 GiB.
|
||||
#[config(
|
||||
env = "TRANQUIL_STORE_EVENTLOG_PENDING_BYTES_BUDGET",
|
||||
default = 1_073_741_824
|
||||
)]
|
||||
pub eventlog_pending_bytes_budget: u64,
|
||||
|
||||
/// Maximum size of an individual eventlog payload in bytes. Single events
|
||||
/// larger than this are rejected at append time. Default: 256 MiB.
|
||||
#[config(
|
||||
env = "TRANQUIL_STORE_EVENTLOG_MAX_EVENT_PAYLOAD",
|
||||
default = 268_435_456
|
||||
)]
|
||||
pub eventlog_max_event_payload: u32,
|
||||
|
||||
/// Maximum size of an individual blockstore data file in bytes. When the
|
||||
/// active data file reaches this size it is rolled over and becomes
|
||||
/// eligible for compaction. Default: 256 MiB.
|
||||
#[config(env = "TRANQUIL_STORE_MAX_BLOCKSTORE_FILE_SIZE", default = 268_435_456)]
|
||||
pub max_blockstore_file_size: u64,
|
||||
|
||||
/// Maximum size of an individual eventlog segment file in bytes. When the
|
||||
/// active segment reaches this size it is sealed and a new one is created.
|
||||
/// Safe to change on a running instance. Default: 256 MiB.
|
||||
#[config(
|
||||
env = "TRANQUIL_STORE_MAX_EVENTLOG_SEGMENT_SIZE",
|
||||
default = 268_435_456
|
||||
)]
|
||||
pub max_eventlog_segment_size: u64,
|
||||
}
|
||||
|
||||
/// Generate a TOML configuration template with all available options,
|
||||
|
||||
@@ -185,12 +185,41 @@ pub struct ReservedSigningKey {
|
||||
pub private_key_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReservedSigningKeyFull {
|
||||
pub id: Uuid,
|
||||
pub did: Option<Did>,
|
||||
pub public_key_did_key: String,
|
||||
pub private_key_bytes: Vec<u8>,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub used_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeletionRequest {
|
||||
pub did: Did,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeletionRequestWithToken {
|
||||
pub token: String,
|
||||
pub did: Did,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlcTokenInfo {
|
||||
pub token: String,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PasswordResetInfo {
|
||||
pub code: Option<String>,
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait InfraRepository: Send + Sync {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -406,6 +435,41 @@ pub trait InfraRepository: Send + Sync {
|
||||
&self,
|
||||
user_ids: &[Uuid],
|
||||
) -> Result<Vec<(Uuid, String)>, DbError>;
|
||||
|
||||
async fn get_deletion_request_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<DeletionRequestWithToken>, DbError>;
|
||||
|
||||
async fn get_latest_comms_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
comms_type: CommsType,
|
||||
limit: i64,
|
||||
) -> Result<Vec<QueuedComms>, DbError>;
|
||||
|
||||
async fn count_comms_by_type(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
comms_type: CommsType,
|
||||
) -> Result<i64, DbError>;
|
||||
|
||||
async fn delete_comms_by_type_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
comms_type: CommsType,
|
||||
) -> Result<u64, DbError>;
|
||||
|
||||
async fn expire_deletion_request(&self, token: &str) -> Result<(), DbError>;
|
||||
|
||||
async fn get_reserved_signing_key_full(
|
||||
&self,
|
||||
public_key_did_key: &str,
|
||||
) -> Result<Option<ReservedSigningKeyFull>, DbError>;
|
||||
|
||||
async fn get_plc_tokens_by_did(&self, did: &Did) -> Result<Vec<PlcTokenInfo>, DbError>;
|
||||
|
||||
async fn count_plc_tokens_by_did(&self, did: &Did) -> Result<i64, DbError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -22,9 +22,10 @@ pub use delegation::{
|
||||
};
|
||||
pub use error::DbError;
|
||||
pub use infra::{
|
||||
AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest, InfraRepository,
|
||||
InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, InviteCodeState, InviteCodeUse,
|
||||
NotificationHistoryRow, QueuedComms, ReservedSigningKey,
|
||||
AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest,
|
||||
DeletionRequestWithToken, InfraRepository, InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder,
|
||||
InviteCodeState, InviteCodeUse, NotificationHistoryRow, PasswordResetInfo, PlcTokenInfo,
|
||||
QueuedComms, ReservedSigningKey, ReservedSigningKeyFull,
|
||||
};
|
||||
pub use invite_code::{InviteCodeError, ValidatedInviteCode};
|
||||
pub use oauth::{
|
||||
@@ -32,11 +33,12 @@ pub use oauth::{
|
||||
ScopePreference, TokenFamilyId, TrustedDeviceRow, TwoFactorChallenge,
|
||||
};
|
||||
pub use repo::{
|
||||
AccountStatus, ApplyCommitError, ApplyCommitInput, ApplyCommitResult, BrokenGenesisCommit,
|
||||
CommitEventData, EventBlocksCids, FullRecordInfo, ImportBlock, ImportRecord, ImportRepoError,
|
||||
RecordDelete, RecordInfo, RecordUpsert, RecordWithTakedown, RepoAccountInfo, RepoEventNotifier,
|
||||
RepoEventReceiver, RepoEventType, RepoInfo, RepoListItem, RepoRepository, RepoSeqEvent,
|
||||
RepoWithoutRev, SequencedEvent, UserNeedingRecordBlobsBackfill, UserWithoutBlocks,
|
||||
AccountStatus, ApplyCommitError, ApplyCommitInput, ApplyCommitResult, CommitEventData,
|
||||
EventBlockInline, EventBlocks, FullRecordInfo, ImportBlock, ImportRecord, ImportRepoError,
|
||||
PruneCount, RecordDelete, RecordInfo, RecordUpsert, RecordWithTakedown, RepoAccountInfo,
|
||||
RepoEventNotifier, RepoEventReceiver, RepoEventType, RepoInfo, RepoListItem, RepoRepository,
|
||||
RepoSeqEvent, RepoWithoutRev, SequencedEvent, UserNeedingRecordBlobsBackfill,
|
||||
UserWithoutBlocks,
|
||||
};
|
||||
pub use scope::{DbScope, InvalidScopeError};
|
||||
pub use sequence::{SequenceNumber, deserialize_optional_sequence};
|
||||
|
||||
@@ -291,21 +291,24 @@ pub trait OAuthRepository: Send + Sync {
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
) -> Result<bool, DbError>;
|
||||
async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError>;
|
||||
async fn revoke_device_trust(&self, device_id: &DeviceId, did: &Did) -> Result<(), DbError>;
|
||||
async fn update_device_friendly_name(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
friendly_name: Option<&str>,
|
||||
) -> Result<(), DbError>;
|
||||
async fn trust_device(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
trusted_at: DateTime<Utc>,
|
||||
trusted_until: DateTime<Utc>,
|
||||
) -> Result<(), DbError>;
|
||||
async fn extend_device_trust(
|
||||
&self,
|
||||
device_id: &DeviceId,
|
||||
did: &Did,
|
||||
trusted_until: DateTime<Utc>,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
@@ -321,4 +324,9 @@ pub trait OAuthRepository: Send + Sync {
|
||||
did: &Did,
|
||||
except_token_id: &TokenId,
|
||||
) -> Result<u64, DbError>;
|
||||
|
||||
async fn get_2fa_challenge_code(
|
||||
&self,
|
||||
request_uri: &RequestId,
|
||||
) -> Result<Option<String>, DbError>;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use tranquil_types::{AtUri, CidLink, Did, Handle, Nsid, Rkey};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::DbError;
|
||||
use crate::backlink::Backlink;
|
||||
use crate::sequence::SequenceNumber;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
|
||||
@@ -164,13 +165,6 @@ pub struct RepoWithoutRev {
|
||||
pub repo_root_cid: CidLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrokenGenesisCommit {
|
||||
pub seq: SequenceNumber,
|
||||
pub did: Did,
|
||||
pub commit_cid: Option<CidLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserWithoutBlocks {
|
||||
pub user_id: Uuid,
|
||||
@@ -189,6 +183,51 @@ pub struct RepoSeqEvent {
|
||||
pub seq: SequenceNumber,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PruneCount {
|
||||
Rows(u64),
|
||||
Segments(u64),
|
||||
}
|
||||
|
||||
impl PruneCount {
|
||||
pub fn is_zero(&self) -> bool {
|
||||
match self {
|
||||
Self::Rows(n) | Self::Segments(n) => *n == 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count(&self) -> u64 {
|
||||
match self {
|
||||
Self::Rows(n) | Self::Segments(n) => *n,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unit(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Rows(_) => "rows",
|
||||
Self::Segments(_) => "segments",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PruneCount {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} {}", self.count(), self.unit())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventBlockInline {
|
||||
pub cid_bytes: Vec<u8>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum EventBlocks {
|
||||
Inline(Vec<EventBlockInline>),
|
||||
LegacyCids(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SequencedEvent {
|
||||
pub seq: SequenceNumber,
|
||||
@@ -200,7 +239,7 @@ pub struct SequencedEvent {
|
||||
pub prev_data_cid: Option<CidLink>,
|
||||
pub ops: Option<serde_json::Value>,
|
||||
pub blobs: Option<Vec<String>>,
|
||||
pub blocks_cids: Option<Vec<String>>,
|
||||
pub blocks: Option<EventBlocks>,
|
||||
pub handle: Option<Handle>,
|
||||
pub active: Option<bool>,
|
||||
pub status: Option<AccountStatus>,
|
||||
@@ -215,17 +254,11 @@ pub struct CommitEventData {
|
||||
pub prev_cid: Option<CidLink>,
|
||||
pub ops: Option<serde_json::Value>,
|
||||
pub blobs: Option<Vec<String>>,
|
||||
pub blocks_cids: Option<Vec<String>>,
|
||||
pub blocks: Option<Vec<EventBlockInline>>,
|
||||
pub prev_data_cid: Option<CidLink>,
|
||||
pub rev: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventBlocksCids {
|
||||
pub blocks_cids: Option<Vec<String>>,
|
||||
pub commit_cid: Option<CidLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RepoListItem {
|
||||
pub did: Did,
|
||||
@@ -279,6 +312,8 @@ pub struct ApplyCommitInput {
|
||||
pub obsolete_block_cids: Vec<Vec<u8>>,
|
||||
pub record_upserts: Vec<RecordUpsert>,
|
||||
pub record_deletes: Vec<RecordDelete>,
|
||||
pub backlinks_to_add: Vec<Backlink>,
|
||||
pub backlinks_to_remove: Vec<AtUri>,
|
||||
pub commit_event: CommitEventData,
|
||||
}
|
||||
|
||||
@@ -300,6 +335,8 @@ pub trait RepoRepository: Send + Sync {
|
||||
async fn create_repo(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
did: &Did,
|
||||
handle: &Handle,
|
||||
repo_root_cid: &CidLink,
|
||||
repo_rev: &str,
|
||||
) -> Result<(), DbError>;
|
||||
@@ -313,6 +350,14 @@ pub trait RepoRepository: Send + Sync {
|
||||
|
||||
async fn update_repo_rev(&self, user_id: Uuid, repo_rev: &str) -> Result<(), DbError>;
|
||||
|
||||
async fn update_repo_status(
|
||||
&self,
|
||||
did: &Did,
|
||||
takedown: Option<bool>,
|
||||
takedown_ref: Option<&str>,
|
||||
deactivated: Option<bool>,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn delete_repo(&self, user_id: Uuid) -> Result<(), DbError>;
|
||||
|
||||
async fn get_repo_root_for_update(&self, user_id: Uuid) -> Result<Option<CidLink>, DbError>;
|
||||
@@ -419,6 +464,7 @@ pub trait RepoRepository: Send + Sync {
|
||||
did: &Did,
|
||||
commit_cid: &CidLink,
|
||||
rev: Option<&str>,
|
||||
commit_bytes: &[u8],
|
||||
) -> Result<SequenceNumber, DbError>;
|
||||
|
||||
async fn insert_genesis_commit_event(
|
||||
@@ -427,20 +473,18 @@ pub trait RepoRepository: Send + Sync {
|
||||
commit_cid: &CidLink,
|
||||
mst_root_cid: &CidLink,
|
||||
rev: &str,
|
||||
commit_bytes: &[u8],
|
||||
mst_root_bytes: &[u8],
|
||||
) -> Result<SequenceNumber, DbError>;
|
||||
|
||||
async fn update_seq_blocks_cids(
|
||||
&self,
|
||||
seq: SequenceNumber,
|
||||
blocks_cids: &[String],
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn delete_sequences_except(
|
||||
&self,
|
||||
did: &Did,
|
||||
keep_seq: SequenceNumber,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn prune_events_older_than(&self, cutoff: DateTime<Utc>) -> Result<PruneCount, DbError>;
|
||||
|
||||
async fn get_max_seq(&self) -> Result<SequenceNumber, DbError>;
|
||||
|
||||
async fn get_min_seq_since(
|
||||
@@ -473,12 +517,6 @@ pub trait RepoRepository: Send + Sync {
|
||||
limit: i64,
|
||||
) -> Result<Vec<SequencedEvent>, DbError>;
|
||||
|
||||
async fn get_events_since_rev(
|
||||
&self,
|
||||
did: &Did,
|
||||
since_rev: &str,
|
||||
) -> Result<Vec<EventBlocksCids>, DbError>;
|
||||
|
||||
async fn list_repos_paginated(
|
||||
&self,
|
||||
cursor_did: Option<&Did>,
|
||||
@@ -503,8 +541,6 @@ pub trait RepoRepository: Send + Sync {
|
||||
input: ApplyCommitInput,
|
||||
) -> Result<ApplyCommitResult, ApplyCommitError>;
|
||||
|
||||
async fn get_broken_genesis_commits(&self) -> Result<Vec<BrokenGenesisCommit>, DbError>;
|
||||
|
||||
async fn get_users_without_blocks(&self) -> Result<Vec<UserWithoutBlocks>, DbError>;
|
||||
|
||||
async fn get_users_needing_record_blobs_backfill(
|
||||
|
||||
@@ -23,6 +23,10 @@ impl SequenceNumber {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.0 >= 0
|
||||
}
|
||||
|
||||
pub fn as_u64(&self) -> Option<u64> {
|
||||
u64::try_from(self.0).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SequenceNumber {
|
||||
|
||||
@@ -223,6 +223,8 @@ pub trait UserRepository: Send + Sync {
|
||||
|
||||
async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError>;
|
||||
|
||||
async fn set_admin_status(&self, did: &Did, is_admin: bool) -> Result<(), DbError>;
|
||||
|
||||
async fn get_notification_prefs(&self, did: &Did)
|
||||
-> Result<Option<NotificationPrefs>, DbError>;
|
||||
|
||||
@@ -584,6 +586,18 @@ pub trait UserRepository: Send + Sync {
|
||||
&self,
|
||||
input: &RecoverPasskeyAccountInput,
|
||||
) -> Result<RecoverPasskeyAccountResult, DbError>;
|
||||
|
||||
async fn get_password_reset_info(
|
||||
&self,
|
||||
email: &str,
|
||||
) -> Result<Option<crate::PasswordResetInfo>, DbError>;
|
||||
|
||||
async fn enable_totp_verified(&self, did: &Did, encrypted_secret: &[u8])
|
||||
-> Result<(), DbError>;
|
||||
|
||||
async fn set_two_factor_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError>;
|
||||
|
||||
async fn expire_password_reset_code(&self, email: &str) -> Result<(), DbError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -3,9 +3,9 @@ use chrono::{DateTime, Utc};
|
||||
use sqlx::PgPool;
|
||||
use tranquil_db_traits::{
|
||||
AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DbError, DeletionRequest,
|
||||
InfraRepository, InviteCodeError, InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder,
|
||||
InviteCodeState, InviteCodeUse, NotificationHistoryRow, QueuedComms, ReservedSigningKey,
|
||||
ValidatedInviteCode,
|
||||
DeletionRequestWithToken, InfraRepository, InviteCodeError, InviteCodeInfo, InviteCodeRow,
|
||||
InviteCodeSortOrder, InviteCodeState, InviteCodeUse, NotificationHistoryRow, PlcTokenInfo,
|
||||
QueuedComms, ReservedSigningKey, ReservedSigningKeyFull, ValidatedInviteCode,
|
||||
};
|
||||
use tranquil_types::{CidLink, Did, Handle};
|
||||
use uuid::Uuid;
|
||||
@@ -1034,4 +1034,159 @@ impl InfraRepository for PostgresInfraRepository {
|
||||
.map(|r| (r.used_by_user, r.code))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_deletion_request_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Option<DeletionRequestWithToken>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT token, did, expires_at FROM account_deletion_requests WHERE did = $1"#,
|
||||
did.as_str()
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(|r| DeletionRequestWithToken {
|
||||
token: r.token,
|
||||
did: Did::new(r.did).expect("valid DID in database"),
|
||||
expires_at: r.expires_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_latest_comms_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
comms_type: CommsType,
|
||||
limit: i64,
|
||||
) -> Result<Vec<QueuedComms>, DbError> {
|
||||
let results = sqlx::query_as!(
|
||||
QueuedComms,
|
||||
r#"SELECT
|
||||
id, user_id,
|
||||
channel as "channel: CommsChannel",
|
||||
comms_type as "comms_type: CommsType",
|
||||
status as "status: CommsStatus",
|
||||
recipient, subject, body, metadata,
|
||||
attempts, max_attempts, last_error,
|
||||
created_at, updated_at, scheduled_for, processed_at
|
||||
FROM comms_queue
|
||||
WHERE user_id = $1 AND comms_type = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $3"#,
|
||||
user_id,
|
||||
comms_type as CommsType,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn count_comms_by_type(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
comms_type: CommsType,
|
||||
) -> Result<i64, DbError> {
|
||||
let count = sqlx::query_scalar!(
|
||||
r#"SELECT COUNT(*) as "count!" FROM comms_queue WHERE user_id = $1 AND comms_type = $2"#,
|
||||
user_id,
|
||||
comms_type as CommsType
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn delete_comms_by_type_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
comms_type: CommsType,
|
||||
) -> Result<u64, DbError> {
|
||||
let result = sqlx::query!(
|
||||
"DELETE FROM comms_queue WHERE user_id = $1 AND comms_type = $2",
|
||||
user_id,
|
||||
comms_type as CommsType
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn expire_deletion_request(&self, token: &str) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
|
||||
token
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_reserved_signing_key_full(
|
||||
&self,
|
||||
public_key_did_key: &str,
|
||||
) -> Result<Option<ReservedSigningKeyFull>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT id, did, public_key_did_key, private_key_bytes, expires_at, used_at
|
||||
FROM reserved_signing_keys WHERE public_key_did_key = $1"#,
|
||||
public_key_did_key
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(|r| ReservedSigningKeyFull {
|
||||
id: r.id,
|
||||
did: r.did.map(|d| Did::new(d).expect("valid DID in database")),
|
||||
public_key_did_key: r.public_key_did_key,
|
||||
private_key_bytes: r.private_key_bytes,
|
||||
expires_at: r.expires_at,
|
||||
used_at: r.used_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_plc_tokens_by_did(&self, did: &Did) -> Result<Vec<PlcTokenInfo>, DbError> {
|
||||
let results = sqlx::query!(
|
||||
r#"SELECT t.token, t.expires_at
|
||||
FROM plc_operation_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.did = $1"#,
|
||||
did.as_str()
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| PlcTokenInfo {
|
||||
token: r.token,
|
||||
expires_at: r.expires_at,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn count_plc_tokens_by_did(&self, did: &Did) -> Result<i64, DbError> {
|
||||
let count = sqlx::query_scalar!(
|
||||
r#"SELECT COUNT(*) as "count!"
|
||||
FROM plc_operation_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE u.did = $1"#,
|
||||
did.as_str()
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use tranquil_db_traits::{
|
||||
pub use user::PostgresUserRepository;
|
||||
|
||||
pub struct PostgresRepositories {
|
||||
pub pool: PgPool,
|
||||
pub pool: Option<PgPool>,
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub oauth: Arc<dyn OAuthRepository>,
|
||||
pub session: Arc<dyn SessionRepository>,
|
||||
@@ -44,7 +44,7 @@ pub struct PostgresRepositories {
|
||||
impl PostgresRepositories {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self {
|
||||
pool: pool.clone(),
|
||||
pool: Some(pool.clone()),
|
||||
user: Arc::new(PostgresUserRepository::new(pool.clone())),
|
||||
oauth: Arc::new(PostgresOAuthRepository::new(pool.clone())),
|
||||
session: Arc::new(PostgresSessionRepository::new(pool.clone())),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user