Our Personal Data Server from scratch!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(tranquil-store): whole test suite working

Lewis: May this revision serve well! <lu5a@proton.me>

+5702 -2975
-22
.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT t.token FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc" 22 - }
-15
.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "UPDATE users SET deactivated_at = $1 WHERE did = $2", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Timestamptz", 9 - "Text" 10 - ] 11 - }, 12 - "nullable": [] 13 - }, 14 - "hash": "0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107" 15 - }
-22
.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "body", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99" 22 - }
-22
.sqlx/query-0fae1be7a75bdc58c69a9af97cad4aec23c32a9378764b8d6d7eb2cc89c562b1.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT token FROM sso_pending_registration WHERE token = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "0fae1be7a75bdc58c69a9af97cad4aec23c32a9378764b8d6d7eb2cc89c562b1" 22 - }
-22
.sqlx/query-1c84643fd6bc57c76517849a64d2d877df337e823d4c2c2b077f695bbfc9e9ac.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "1c84643fd6bc57c76517849a64d2d877df337e823d4c2c2b077f695bbfc9e9ac" 22 - }
+15
.sqlx/query-1e63d287c619a14e5c07d80e8e54193d2964c8b5e6a855256cb80c4d0cd2c6ea.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE users SET is_admin = $1 WHERE did = $2", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Bool", 9 + "Text" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "1e63d287c619a14e5c07d80e8e54193d2964c8b5e6a855256cb80c4d0cd2c6ea" 15 + }
-28
.sqlx/query-24b823043ab60f36c29029137fef30dfe33922bb06067f2fdbfc1fbb4b0a2a81.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token, request_uri\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "request_uri", 14 - "type_info": "Text" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - false, 24 - false 25 - ] 26 - }, 27 - "hash": "24b823043ab60f36c29029137fef30dfe33922bb06067f2fdbfc1fbb4b0a2a81" 28 - }
-38
.sqlx/query-2841093a67480e75e1e9e4046bf3eb74afae2d04f5ea0ec17a4d433983e6d71c.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n RETURNING id\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "id", 9 - "type_info": "Uuid" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text", 15 - { 16 - "Custom": { 17 - "name": "sso_provider_type", 18 - "kind": { 19 - "Enum": [ 20 - "github", 21 - "discord", 22 - "google", 23 - "gitlab", 24 - "oidc", 25 - "apple" 26 - ] 27 - } 28 - } 29 - }, 30 - "Text" 31 - ] 32 - }, 33 - "nullable": [ 34 - false 35 - ] 36 - }, 37 - "hash": "2841093a67480e75e1e9e4046bf3eb74afae2d04f5ea0ec17a4d433983e6d71c" 38 - }
-32
.sqlx/query-376b72306b50f747bc9161985ff4f50c35c53025a55ccf5e9933dc3795d29313.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5)\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - { 11 - "Custom": { 12 - "name": "sso_provider_type", 13 - "kind": { 14 - "Enum": [ 15 - "github", 16 - "discord", 17 - "google", 18 - "gitlab", 19 - "oidc", 20 - "apple" 21 - ] 22 - } 23 - } 24 - }, 25 - "Text", 26 - "Bool" 27 - ] 28 - }, 29 - "nullable": [] 30 - }, 31 - "hash": "376b72306b50f747bc9161985ff4f50c35c53025a55ccf5e9933dc3795d29313" 32 - }
-22
.sqlx/query-3933ea5b147ab6294936de147b98e116cfae848ecd76ea5d367585eb5117f2ad.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT id FROM external_identities WHERE id = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "id", 9 - "type_info": "Uuid" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Uuid" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "3933ea5b147ab6294936de147b98e116cfae848ecd76ea5d367585eb5117f2ad" 22 - }
-16
.sqlx/query-3bed8d4843545f4a9676207513806603c50eb2af92957994abaf1c89c0294c12.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - "Text" 11 - ] 12 - }, 13 - "nullable": [] 14 - }, 15 - "hash": "3bed8d4843545f4a9676207513806603c50eb2af92957994abaf1c89c0294c12" 16 - }
-55
.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT subject, body, comms_type as \"comms_type: String\" FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' ORDER BY created_at DESC LIMIT 1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "subject", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "body", 14 - "type_info": "Text" 15 - }, 16 - { 17 - "ordinal": 2, 18 - "name": "comms_type: String", 19 - "type_info": { 20 - "Custom": { 21 - "name": "comms_type", 22 - "kind": { 23 - "Enum": [ 24 - "welcome", 25 - "email_verification", 26 - "password_reset", 27 - "email_update", 28 - "account_deletion", 29 - "admin_email", 30 - "plc_operation", 31 - "two_factor_code", 32 - "channel_verification", 33 - "passkey_recovery", 34 - "legacy_login_alert", 35 - "migration_verification", 36 - "channel_verified" 37 - ] 38 - } 39 - } 40 - } 41 - } 42 - ], 43 - "parameters": { 44 - "Left": [ 45 - "Uuid" 46 - ] 47 - }, 48 - "nullable": [ 49 - true, 50 - false, 51 - false 52 - ] 53 - }, 54 - "hash": "4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe" 55 - }
-22
.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT id FROM users WHERE email = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "id", 9 - "type_info": "Uuid" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068" 22 - }
+2 -2
.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json .sqlx/query-237c2d912e89b7e0e5baa83503a22f158ea1614b5157f6c9e2aba6017fef6b26.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1", 3 + "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", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 24 24 false 25 25 ] 26 26 }, 27 - "hash": "47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88" 27 + "hash": "237c2d912e89b7e0e5baa83503a22f158ea1614b5157f6c9e2aba6017fef6b26" 28 28 }
-22
.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT email_verified FROM users WHERE did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "email_verified", 9 - "type_info": "Bool" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6" 22 - }
-22
.sqlx/query-4fef326fa2d03d04869af3fec702c901d1ecf392545a3a032438b2c1859d46cc.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT token FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "4fef326fa2d03d04869af3fec702c901d1ecf392545a3a032438b2c1859d46cc" 22 - }
-15
.sqlx/query-575c1e5529874f8f523e6fe22ccf4ee3296806581b1765dfb91a84ffab347f15.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO oauth_authorization_request (id, client_id, parameters, expires_at)\n VALUES ($1, 'https://test.example.com', $2, NOW() + INTERVAL '1 hour')\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Jsonb" 10 - ] 11 - }, 12 - "nullable": [] 13 - }, 14 - "hash": "575c1e5529874f8f523e6fe22ccf4ee3296806581b1765dfb91a84ffab347f15" 15 - }
-33
.sqlx/query-596c3400a60c77c7645fd46fcea61fa7898b6832e58c0f647f382b23b81d350e.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - { 11 - "Custom": { 12 - "name": "sso_provider_type", 13 - "kind": { 14 - "Enum": [ 15 - "github", 16 - "discord", 17 - "google", 18 - "gitlab", 19 - "oidc", 20 - "apple" 21 - ] 22 - } 23 - } 24 - }, 25 - "Text", 26 - "Text", 27 - "Text" 28 - ] 29 - }, 30 - "nullable": [] 31 - }, 32 - "hash": "596c3400a60c77c7645fd46fcea61fa7898b6832e58c0f647f382b23b81d350e" 33 - }
-81
.sqlx/query-59e63c5cf92985714e9586d1ce012efef733d4afaa4ea09974daf8303805e5d2.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT id, did, provider as \"provider: SsoProviderType\", provider_user_id, provider_username, provider_email\n FROM external_identities\n WHERE provider = $1 AND provider_user_id = $2\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "id", 9 - "type_info": "Uuid" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "did", 14 - "type_info": "Text" 15 - }, 16 - { 17 - "ordinal": 2, 18 - "name": "provider: SsoProviderType", 19 - "type_info": { 20 - "Custom": { 21 - "name": "sso_provider_type", 22 - "kind": { 23 - "Enum": [ 24 - "github", 25 - "discord", 26 - "google", 27 - "gitlab", 28 - "oidc", 29 - "apple" 30 - ] 31 - } 32 - } 33 - } 34 - }, 35 - { 36 - "ordinal": 3, 37 - "name": "provider_user_id", 38 - "type_info": "Text" 39 - }, 40 - { 41 - "ordinal": 4, 42 - "name": "provider_username", 43 - "type_info": "Text" 44 - }, 45 - { 46 - "ordinal": 5, 47 - "name": "provider_email", 48 - "type_info": "Text" 49 - } 50 - ], 51 - "parameters": { 52 - "Left": [ 53 - { 54 - "Custom": { 55 - "name": "sso_provider_type", 56 - "kind": { 57 - "Enum": [ 58 - "github", 59 - "discord", 60 - "google", 61 - "gitlab", 62 - "oidc", 63 - "apple" 64 - ] 65 - } 66 - } 67 - }, 68 - "Text" 69 - ] 70 - }, 71 - "nullable": [ 72 - false, 73 - false, 74 - false, 75 - false, 76 - true, 77 - true 78 - ] 79 - }, 80 - "hash": "59e63c5cf92985714e9586d1ce012efef733d4afaa4ea09974daf8303805e5d2" 81 - }
-28
.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT k.key_bytes, k.encryption_version\n FROM user_keys k\n JOIN users u ON k.user_id = u.id\n WHERE u.did = $1\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "key_bytes", 9 - "type_info": "Bytea" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "encryption_version", 14 - "type_info": "Int4" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - false, 24 - true 25 - ] 26 - }, 27 - "hash": "5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0" 28 - }
-22
.sqlx/query-5af4a386c1632903ad7102551a5bd148bcf541baab6a84c8649666a695f9c4d1.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "state", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "5af4a386c1632903ad7102551a5bd148bcf541baab6a84c8649666a695f9c4d1" 22 - }
+22
.sqlx/query-5cee16f49a727d66b5231a8d07d7f4bcb6a1136fbf3e3d249fd33600772ac80f.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "code", 9 + "type_info": "Text" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + false 19 + ] 20 + }, 21 + "hash": "5cee16f49a727d66b5231a8d07d7f4bcb6a1136fbf3e3d249fd33600772ac80f" 22 + }
-43
.sqlx/query-5e4c0dd92ac3c4b5e2eae5d129f2649cf3a8f068105f44a8dca9625427affc06.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT provider_user_id, provider_email_verified\n FROM external_identities\n WHERE did = $1 AND provider = $2\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "provider_user_id", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "provider_email_verified", 14 - "type_info": "Bool" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text", 20 - { 21 - "Custom": { 22 - "name": "sso_provider_type", 23 - "kind": { 24 - "Enum": [ 25 - "github", 26 - "discord", 27 - "google", 28 - "gitlab", 29 - "oidc", 30 - "apple" 31 - ] 32 - } 33 - } 34 - } 35 - ] 36 - }, 37 - "nullable": [ 38 - false, 39 - false 40 - ] 41 - }, 42 - "hash": "5e4c0dd92ac3c4b5e2eae5d129f2649cf3a8f068105f44a8dca9625427affc06" 43 - }
-33
.sqlx/query-5e9c6ec72c2c0ea1c8dff551d01baddd1dd953c828a5656db2ee39dea996f890.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, nonce, code_verifier)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - { 11 - "Custom": { 12 - "name": "sso_provider_type", 13 - "kind": { 14 - "Enum": [ 15 - "github", 16 - "discord", 17 - "google", 18 - "gitlab", 19 - "oidc", 20 - "apple" 21 - ] 22 - } 23 - } 24 - }, 25 - "Text", 26 - "Text", 27 - "Text" 28 - ] 29 - }, 30 - "nullable": [] 31 - }, 32 - "hash": "5e9c6ec72c2c0ea1c8dff551d01baddd1dd953c828a5656db2ee39dea996f890" 33 - }
+34
.sqlx/query-61f489b4fc42f5b0aaea287cde4415da6f5e96b3a0f36216bdc6dea924b09abd.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT token, did, expires_at FROM account_deletion_requests WHERE did = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "token", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "did", 14 + "type_info": "Text" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "expires_at", 19 + "type_info": "Timestamptz" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Text" 25 + ] 26 + }, 27 + "nullable": [ 28 + false, 29 + false, 30 + false 31 + ] 32 + }, 33 + "hash": "61f489b4fc42f5b0aaea287cde4415da6f5e96b3a0f36216bdc6dea924b09abd" 34 + }
-28
.sqlx/query-63f6f2a89650794fe90e10ce7fc785a6b9f7d37c12b31a6ff13f7c5214eef19e.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT did, email_verified FROM users WHERE did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "did", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "email_verified", 14 - "type_info": "Bool" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - false, 24 - false 25 - ] 26 - }, 27 - "hash": "63f6f2a89650794fe90e10ce7fc785a6b9f7d37c12b31a6ff13f7c5214eef19e" 28 - }
-66
.sqlx/query-6c7ace2a64848adc757af6c93b9162e1d95788b372370a7ad0d7540338bb73ee.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT state, request_uri, provider as \"provider: SsoProviderType\", action, nonce, code_verifier\n FROM sso_auth_state\n WHERE state = $1\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "state", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "request_uri", 14 - "type_info": "Text" 15 - }, 16 - { 17 - "ordinal": 2, 18 - "name": "provider: SsoProviderType", 19 - "type_info": { 20 - "Custom": { 21 - "name": "sso_provider_type", 22 - "kind": { 23 - "Enum": [ 24 - "github", 25 - "discord", 26 - "google", 27 - "gitlab", 28 - "oidc", 29 - "apple" 30 - ] 31 - } 32 - } 33 - } 34 - }, 35 - { 36 - "ordinal": 3, 37 - "name": "action", 38 - "type_info": "Text" 39 - }, 40 - { 41 - "ordinal": 4, 42 - "name": "nonce", 43 - "type_info": "Text" 44 - }, 45 - { 46 - "ordinal": 5, 47 - "name": "code_verifier", 48 - "type_info": "Text" 49 - } 50 - ], 51 - "parameters": { 52 - "Left": [ 53 - "Text" 54 - ] 55 - }, 56 - "nullable": [ 57 - false, 58 - false, 59 - false, 60 - false, 61 - true, 62 - true 63 - ] 64 - }, 65 - "hash": "6c7ace2a64848adc757af6c93b9162e1d95788b372370a7ad0d7540338bb73ee" 66 - }
-22
.sqlx/query-6fbcff0206599484bfb6cef165b6f729d27e7a342f7718ee4ac07f0ca94412ba.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT state FROM sso_auth_state WHERE state = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "state", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "6fbcff0206599484bfb6cef165b6f729d27e7a342f7718ee4ac07f0ca94412ba" 22 - }
-33
.sqlx/query-712459c27fc037f45389e2766cf1057e86e93ef756a784ed12beb453b03c5da1.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - { 11 - "Custom": { 12 - "name": "sso_provider_type", 13 - "kind": { 14 - "Enum": [ 15 - "github", 16 - "discord", 17 - "google", 18 - "gitlab", 19 - "oidc", 20 - "apple" 21 - ] 22 - } 23 - } 24 - }, 25 - "Text", 26 - "Text", 27 - "Bool" 28 - ] 29 - }, 30 - "nullable": [] 31 - }, 32 - "hash": "712459c27fc037f45389e2766cf1057e86e93ef756a784ed12beb453b03c5da1" 33 - }
-22
.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "subject", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Uuid" 15 - ] 16 - }, 17 - "nullable": [ 18 - true 19 - ] 20 - }, 21 - "hash": "785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f" 22 - }
-22
.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT t.token\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631" 22 - }
-28
.sqlx/query-7d24e744a4e63570b1410e50b45b745ce8915ab3715b3eff7efc2d84f27735d0.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT provider_username, last_login_at FROM external_identities WHERE id = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "provider_username", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "last_login_at", 14 - "type_info": "Timestamptz" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Uuid" 20 - ] 21 - }, 22 - "nullable": [ 23 - true, 24 - true 25 - ] 26 - }, 27 - "hash": "7d24e744a4e63570b1410e50b45b745ce8915ab3715b3eff7efc2d84f27735d0" 28 - }
+15
.sqlx/query-7d3a9f0545943bc6a3a14fcd596aac5cc731c8177d74e504606d7e92c7d0c73f.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "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", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text", 9 + "Bytea" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "7d3a9f0545943bc6a3a14fcd596aac5cc731c8177d74e504606d7e92c7d0c73f" 15 + }
-28
.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT t.token, t.expires_at FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "expires_at", 14 - "type_info": "Timestamptz" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - false, 24 - false 25 - ] 26 - }, 27 - "hash": "82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81" 28 - }
+15
.sqlx/query-84a1db51a98402323cb86bc19cd2b737f908222ea3426b8bf47d735aff5b6c75.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE users SET two_factor_enabled = $1 WHERE did = $2", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Bool", 9 + "Text" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "84a1db51a98402323cb86bc19cd2b737f908222ea3426b8bf47d735aff5b6c75" 15 + }
-34
.sqlx/query-85ffc37a77af832d7795f5f37efe304fced4bf56b4f2287fe9aeb3fc97e1b191.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - { 11 - "Custom": { 12 - "name": "sso_provider_type", 13 - "kind": { 14 - "Enum": [ 15 - "github", 16 - "discord", 17 - "google", 18 - "gitlab", 19 - "oidc", 20 - "apple" 21 - ] 22 - } 23 - } 24 - }, 25 - "Text", 26 - "Text", 27 - "Text", 28 - "Bool" 29 - ] 30 - }, 31 - "nullable": [] 32 - }, 33 - "hash": "85ffc37a77af832d7795f5f37efe304fced4bf56b4f2287fe9aeb3fc97e1b191" 34 - }
+36
.sqlx/query-89b0292d8d022fad8f9cda07b9a7870ca6a7ebe904b2d580956b0816b50bcdb7.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "DELETE FROM comms_queue WHERE user_id = $1 AND comms_type = $2", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Uuid", 9 + { 10 + "Custom": { 11 + "name": "comms_type", 12 + "kind": { 13 + "Enum": [ 14 + "welcome", 15 + "email_verification", 16 + "password_reset", 17 + "email_update", 18 + "account_deletion", 19 + "admin_email", 20 + "plc_operation", 21 + "two_factor_code", 22 + "channel_verification", 23 + "passkey_recovery", 24 + "legacy_login_alert", 25 + "migration_verification", 26 + "channel_verified" 27 + ] 28 + } 29 + } 30 + } 31 + ] 32 + }, 33 + "nullable": [] 34 + }, 35 + "hash": "89b0292d8d022fad8f9cda07b9a7870ca6a7ebe904b2d580956b0816b50bcdb7" 36 + }
+22
.sqlx/query-990bf50e60fc5566639c2c12cd968d154d7b0c6863ad69141653135f98fbc998.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "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", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "count!", 9 + "type_info": "Int8" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + null 19 + ] 20 + }, 21 + "hash": "990bf50e60fc5566639c2c12cd968d154d7b0c6863ad69141653135f98fbc998" 22 + }
-22
.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "body", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5" 22 - }
-28
.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "did", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "public_key_did_key", 14 - "type_info": "Text" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - true, 24 - false 25 - ] 26 - }, 27 - "hash": "9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002" 28 - }
-22
.sqlx/query-9dba64081d4f95b5490c9a9bf30a7175db3429f39df4f25e212f38f33882fc65.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT id FROM external_identities WHERE did = $1\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "id", 9 - "type_info": "Uuid" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "9dba64081d4f95b5490c9a9bf30a7175db3429f39df4f25e212f38f33882fc65" 22 - }
+180
.sqlx/query-9f3f2b36f11e9446915d3ca29ef81e4ada0c6a6d72764116dac4f99a4e09785e.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "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", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Uuid" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "user_id", 14 + "type_info": "Uuid" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "channel: CommsChannel", 19 + "type_info": { 20 + "Custom": { 21 + "name": "comms_channel", 22 + "kind": { 23 + "Enum": [ 24 + "email", 25 + "discord", 26 + "telegram", 27 + "signal" 28 + ] 29 + } 30 + } 31 + } 32 + }, 33 + { 34 + "ordinal": 3, 35 + "name": "comms_type: CommsType", 36 + "type_info": { 37 + "Custom": { 38 + "name": "comms_type", 39 + "kind": { 40 + "Enum": [ 41 + "welcome", 42 + "email_verification", 43 + "password_reset", 44 + "email_update", 45 + "account_deletion", 46 + "admin_email", 47 + "plc_operation", 48 + "two_factor_code", 49 + "channel_verification", 50 + "passkey_recovery", 51 + "legacy_login_alert", 52 + "migration_verification", 53 + "channel_verified" 54 + ] 55 + } 56 + } 57 + } 58 + }, 59 + { 60 + "ordinal": 4, 61 + "name": "status: CommsStatus", 62 + "type_info": { 63 + "Custom": { 64 + "name": "comms_status", 65 + "kind": { 66 + "Enum": [ 67 + "pending", 68 + "processing", 69 + "sent", 70 + "failed" 71 + ] 72 + } 73 + } 74 + } 75 + }, 76 + { 77 + "ordinal": 5, 78 + "name": "recipient", 79 + "type_info": "Text" 80 + }, 81 + { 82 + "ordinal": 6, 83 + "name": "subject", 84 + "type_info": "Text" 85 + }, 86 + { 87 + "ordinal": 7, 88 + "name": "body", 89 + "type_info": "Text" 90 + }, 91 + { 92 + "ordinal": 8, 93 + "name": "metadata", 94 + "type_info": "Jsonb" 95 + }, 96 + { 97 + "ordinal": 9, 98 + "name": "attempts", 99 + "type_info": "Int4" 100 + }, 101 + { 102 + "ordinal": 10, 103 + "name": "max_attempts", 104 + "type_info": "Int4" 105 + }, 106 + { 107 + "ordinal": 11, 108 + "name": "last_error", 109 + "type_info": "Text" 110 + }, 111 + { 112 + "ordinal": 12, 113 + "name": "created_at", 114 + "type_info": "Timestamptz" 115 + }, 116 + { 117 + "ordinal": 13, 118 + "name": "updated_at", 119 + "type_info": "Timestamptz" 120 + }, 121 + { 122 + "ordinal": 14, 123 + "name": "scheduled_for", 124 + "type_info": "Timestamptz" 125 + }, 126 + { 127 + "ordinal": 15, 128 + "name": "processed_at", 129 + "type_info": "Timestamptz" 130 + } 131 + ], 132 + "parameters": { 133 + "Left": [ 134 + "Uuid", 135 + { 136 + "Custom": { 137 + "name": "comms_type", 138 + "kind": { 139 + "Enum": [ 140 + "welcome", 141 + "email_verification", 142 + "password_reset", 143 + "email_update", 144 + "account_deletion", 145 + "admin_email", 146 + "plc_operation", 147 + "two_factor_code", 148 + "channel_verification", 149 + "passkey_recovery", 150 + "legacy_login_alert", 151 + "migration_verification", 152 + "channel_verified" 153 + ] 154 + } 155 + } 156 + }, 157 + "Int8" 158 + ] 159 + }, 160 + "nullable": [ 161 + false, 162 + false, 163 + false, 164 + false, 165 + false, 166 + false, 167 + true, 168 + false, 169 + true, 170 + false, 171 + false, 172 + true, 173 + false, 174 + false, 175 + false, 176 + true 177 + ] 178 + }, 179 + "hash": "9f3f2b36f11e9446915d3ca29ef81e4ada0c6a6d72764116dac4f99a4e09785e" 180 + }
-66
.sqlx/query-9fd56986c1c843d386d1e5884acef8573eb55a3e9f5cb0122fcf8b93d6d667a5.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT token, request_uri, provider as \"provider: SsoProviderType\", provider_user_id,\n provider_username, provider_email\n FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "request_uri", 14 - "type_info": "Text" 15 - }, 16 - { 17 - "ordinal": 2, 18 - "name": "provider: SsoProviderType", 19 - "type_info": { 20 - "Custom": { 21 - "name": "sso_provider_type", 22 - "kind": { 23 - "Enum": [ 24 - "github", 25 - "discord", 26 - "google", 27 - "gitlab", 28 - "oidc", 29 - "apple" 30 - ] 31 - } 32 - } 33 - } 34 - }, 35 - { 36 - "ordinal": 3, 37 - "name": "provider_user_id", 38 - "type_info": "Text" 39 - }, 40 - { 41 - "ordinal": 4, 42 - "name": "provider_username", 43 - "type_info": "Text" 44 - }, 45 - { 46 - "ordinal": 5, 47 - "name": "provider_email", 48 - "type_info": "Text" 49 - } 50 - ], 51 - "parameters": { 52 - "Left": [ 53 - "Text" 54 - ] 55 - }, 56 - "nullable": [ 57 - false, 58 - false, 59 - false, 60 - false, 61 - true, 62 - true 63 - ] 64 - }, 65 - "hash": "9fd56986c1c843d386d1e5884acef8573eb55a3e9f5cb0122fcf8b93d6d667a5" 66 - }
-34
.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "private_key_bytes", 9 - "type_info": "Bytea" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "expires_at", 14 - "type_info": "Timestamptz" 15 - }, 16 - { 17 - "ordinal": 2, 18 - "name": "used_at", 19 - "type_info": "Timestamptz" 20 - } 21 - ], 22 - "parameters": { 23 - "Left": [ 24 - "Text" 25 - ] 26 - }, 27 - "nullable": [ 28 - false, 29 - false, 30 - true 31 - ] 32 - }, 33 - "hash": "a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5" 34 - }
-15
.sqlx/query-a3d549a32e76c24e265c73a98dd739067623f275de0740bd576ee288f4444496.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n UPDATE external_identities\n SET provider_username = $2, last_login_at = NOW()\n WHERE id = $1\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Uuid", 9 - "Text" 10 - ] 11 - }, 12 - "nullable": [] 13 - }, 14 - "hash": "a3d549a32e76c24e265c73a98dd739067623f275de0740bd576ee288f4444496" 15 - }
-22
.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT token FROM account_deletion_requests WHERE did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "token", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5" 22 - }
-40
.sqlx/query-a844774d8dd3c50c5faf3de5d43f534b80234759c8437434e467ca33ea10fd1f.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT preferred_comms_channel as \"preferred_comms_channel: String\", discord_username FROM users WHERE did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "preferred_comms_channel: String", 9 - "type_info": { 10 - "Custom": { 11 - "name": "comms_channel", 12 - "kind": { 13 - "Enum": [ 14 - "email", 15 - "discord", 16 - "telegram", 17 - "signal" 18 - ] 19 - } 20 - } 21 - } 22 - }, 23 - { 24 - "ordinal": 1, 25 - "name": "discord_username", 26 - "type_info": "Text" 27 - } 28 - ], 29 - "parameters": { 30 - "Left": [ 31 - "Text" 32 - ] 33 - }, 34 - "nullable": [ 35 - false, 36 - true 37 - ] 38 - }, 39 - "hash": "a844774d8dd3c50c5faf3de5d43f534b80234759c8437434e467ca33ea10fd1f" 40 - }
-28
.sqlx/query-aee3e8e1d8924d41bec7d866e274f8bb2ddef833eb03326103c2d0a17ee56154.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state, request_uri\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "state", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "request_uri", 14 - "type_info": "Text" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Text" 20 - ] 21 - }, 22 - "nullable": [ 23 - false, 24 - false 25 - ] 26 - }, 27 - "hash": "aee3e8e1d8924d41bec7d866e274f8bb2ddef833eb03326103c2d0a17ee56154" 28 - }
+44
.sqlx/query-b364a2b202bab17c0cdc5f70d23b13841b4d9063d94cd0b09268c3dc41824fd2.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT COUNT(*) as \"count!\" FROM comms_queue WHERE user_id = $1 AND comms_type = $2", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "count!", 9 + "type_info": "Int8" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Uuid", 15 + { 16 + "Custom": { 17 + "name": "comms_type", 18 + "kind": { 19 + "Enum": [ 20 + "welcome", 21 + "email_verification", 22 + "password_reset", 23 + "email_update", 24 + "account_deletion", 25 + "admin_email", 26 + "plc_operation", 27 + "two_factor_code", 28 + "channel_verification", 29 + "passkey_recovery", 30 + "legacy_login_alert", 31 + "migration_verification", 32 + "channel_verified" 33 + ] 34 + } 35 + } 36 + } 37 + ] 38 + }, 39 + "nullable": [ 40 + null 41 + ] 42 + }, 43 + "hash": "b364a2b202bab17c0cdc5f70d23b13841b4d9063d94cd0b09268c3dc41824fd2" 44 + }
-31
.sqlx/query-ba9684872fad5201b8504c2606c29364a2df9631fe98817e7bfacd3f3f51f6cb.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - { 11 - "Custom": { 12 - "name": "sso_provider_type", 13 - "kind": { 14 - "Enum": [ 15 - "github", 16 - "discord", 17 - "google", 18 - "gitlab", 19 - "oidc", 20 - "apple" 21 - ] 22 - } 23 - } 24 - }, 25 - "Text" 26 - ] 27 - }, 28 - "nullable": [] 29 - }, 30 - "hash": "ba9684872fad5201b8504c2606c29364a2df9631fe98817e7bfacd3f3f51f6cb" 31 - }
-12
.sqlx/query-bb4460f75d30f48b79d71b97f2c7d54190260deba2d2ade177dbdaa507ab275b.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "DELETE FROM sso_auth_state WHERE expires_at < NOW()", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [] 8 - }, 9 - "nullable": [] 10 - }, 11 - "hash": "bb4460f75d30f48b79d71b97f2c7d54190260deba2d2ade177dbdaa507ab275b" 12 - }
-22
.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT password_reset_code FROM users WHERE email = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "password_reset_code", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - true 19 - ] 20 - }, 21 - "hash": "cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee" 22 - }
-22
.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT COUNT(*) as \"count!\" FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "count!", 9 - "type_info": "Int8" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - null 19 - ] 20 - }, 21 - "hash": "cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3" 22 - }
-31
.sqlx/query-d0d4fb4b44cda3442b20037b4d5efaa032e1d004c775e2b6077c5050d7d62041.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - "Text", 10 - { 11 - "Custom": { 12 - "name": "sso_provider_type", 13 - "kind": { 14 - "Enum": [ 15 - "github", 16 - "discord", 17 - "google", 18 - "gitlab", 19 - "oidc", 20 - "apple" 21 - ] 22 - } 23 - } 24 - }, 25 - "Text" 26 - ] 27 - }, 28 - "nullable": [] 29 - }, 30 - "hash": "d0d4fb4b44cda3442b20037b4d5efaa032e1d004c775e2b6077c5050d7d62041" 31 - }
-40
.sqlx/query-dd7d80d4d118a5fc95b574e2ca9ffaccf974e52fb6ac368f716409c55f9d3ab0.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "id", 9 - "type_info": "Uuid" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text", 15 - { 16 - "Custom": { 17 - "name": "sso_provider_type", 18 - "kind": { 19 - "Enum": [ 20 - "github", 21 - "discord", 22 - "google", 23 - "gitlab", 24 - "oidc", 25 - "apple" 26 - ] 27 - } 28 - } 29 - }, 30 - "Text", 31 - "Text", 32 - "Text" 33 - ] 34 - }, 35 - "nullable": [ 36 - false 37 - ] 38 - }, 39 - "hash": "dd7d80d4d118a5fc95b574e2ca9ffaccf974e52fb6ac368f716409c55f9d3ab0" 40 - }
-22
.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "count", 9 - "type_info": "Int8" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Uuid" 15 - ] 16 - }, 17 - "nullable": [ 18 - null 19 - ] 20 - }, 21 - "hash": "e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3" 22 - }
-22
.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "used_at", 9 - "type_info": "Timestamptz" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - true 19 - ] 20 - }, 21 - "hash": "e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9" 22 - }
-30
.sqlx/query-eb54d2ce02cab7c2e7f9926bd469b19e5f0513f47173b2738fc01a57082d7abb.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n ", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text", 9 - { 10 - "Custom": { 11 - "name": "sso_provider_type", 12 - "kind": { 13 - "Enum": [ 14 - "github", 15 - "discord", 16 - "google", 17 - "gitlab", 18 - "oidc", 19 - "apple" 20 - ] 21 - } 22 - } 23 - }, 24 - "Text" 25 - ] 26 - }, 27 - "nullable": [] 28 - }, 29 - "hash": "eb54d2ce02cab7c2e7f9926bd469b19e5f0513f47173b2738fc01a57082d7abb" 30 - }
-15
.sqlx/query-ec22a8cc89e480c403a239eac44288e144d83364129491de6156760616666d3d.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "DELETE FROM external_identities WHERE id = $1 AND did = $2", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Uuid", 9 - "Text" 10 - ] 11 - }, 12 - "nullable": [] 13 - }, 14 - "hash": "ec22a8cc89e480c403a239eac44288e144d83364129491de6156760616666d3d" 15 - }
-22
.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT email FROM users WHERE did = $1", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "email", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - true 19 - ] 20 - }, 21 - "hash": "f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7" 22 - }
-14
.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "UPDATE users SET is_admin = TRUE WHERE did = $1", 4 - "describe": { 5 - "columns": [], 6 - "parameters": { 7 - "Left": [ 8 - "Text" 9 - ] 10 - }, 11 - "nullable": [] 12 - }, 13 - "hash": "f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814" 14 - }
+52
.sqlx/query-f3b07f153284b6dd1f22c098af8628d85dcdf20dd6273443ff98d02b6f5ecbf1.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "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", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Uuid" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "did", 14 + "type_info": "Text" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "public_key_did_key", 19 + "type_info": "Text" 20 + }, 21 + { 22 + "ordinal": 3, 23 + "name": "private_key_bytes", 24 + "type_info": "Bytea" 25 + }, 26 + { 27 + "ordinal": 4, 28 + "name": "expires_at", 29 + "type_info": "Timestamptz" 30 + }, 31 + { 32 + "ordinal": 5, 33 + "name": "used_at", 34 + "type_info": "Timestamptz" 35 + } 36 + ], 37 + "parameters": { 38 + "Left": [ 39 + "Text" 40 + ] 41 + }, 42 + "nullable": [ 43 + false, 44 + true, 45 + false, 46 + false, 47 + false, 48 + true 49 + ] 50 + }, 51 + "hash": "f3b07f153284b6dd1f22c098af8628d85dcdf20dd6273443ff98d02b6f5ecbf1" 52 + }
+2
Cargo.lock
··· 7715 7715 "tower-http", 7716 7716 "tower-layer", 7717 7717 "tracing", 7718 + "tracing-subscriber", 7718 7719 "tranquil-api", 7719 7720 "tranquil-auth", 7720 7721 "tranquil-cache", ··· 7821 7822 dependencies = [ 7822 7823 "async-trait", 7823 7824 "chrono", 7825 + "fjall", 7824 7826 "futures", 7825 7827 "presage", 7826 7828 "rand 0.9.2",
+2 -2
Cargo.toml
··· 26 26 ] 27 27 28 28 [workspace.package] 29 - version = "0.4.7" 29 + version = "0.5.0" 30 30 edition = "2024" 31 31 license = "AGPL-3.0-or-later" 32 32 ··· 51 51 tranquil-sync = { path = "crates/tranquil-sync" } 52 52 tranquil-oauth-server = { path = "crates/tranquil-oauth-server" } 53 53 tranquil-api = { path = "crates/tranquil-api" } 54 - tranquil-signal = { path = "crates/tranquil-signal" } 54 + tranquil-signal = { path = "crates/tranquil-signal", features = ["fjall-store"] } 55 55 tranquil-store = { path = "crates/tranquil-store" } 56 56 57 57 presage = { git = "https://github.com/whisperfish/presage", rev = "fe3ed54c4844ae51c3a9fa49cf80a7816a31a425", default-features = false }
+1
Dockerfile
··· 29 29 COPY crates/tranquil-sync ./crates/tranquil-sync 30 30 COPY crates/tranquil-api ./crates/tranquil-api 31 31 COPY crates/tranquil-oauth-server ./crates/tranquil-oauth-server 32 + COPY crates/tranquil-store ./crates/tranquil-store 32 33 COPY crates/tranquil-signal ./crates/tranquil-signal 33 34 COPY crates/tranquil-server ./crates/tranquil-server 34 35 COPY migrations ./crates/tranquil-pds/migrations
+2 -4
crates/tranquil-api/src/admin/account/search.rs
··· 51 51 Query(params): Query<SearchAccountsParams>, 52 52 ) -> Result<Json<SearchAccountsOutput>, ApiError> { 53 53 let limit = params.limit.clamp(1, 100); 54 - let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 55 - let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 56 54 let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok()); 57 55 let rows = state 58 56 .repos 59 57 .user 60 58 .search_accounts( 61 59 cursor_did.as_ref(), 62 - email_filter.as_deref(), 63 - handle_filter.as_deref(), 60 + params.email.as_deref(), 61 + params.handle.as_deref(), 64 62 limit + 1, 65 63 ) 66 64 .await
+21 -13
crates/tranquil-api/src/admin/signal.rs
··· 5 5 use tranquil_pds::api::error::ApiError; 6 6 use tranquil_pds::auth::{Admin, Auth}; 7 7 use tranquil_pds::state::AppState; 8 - use tranquil_signal::PgSignalStore; 9 8 10 9 #[derive(Serialize)] 11 10 #[serde(rename_all = "camelCase")] ··· 53 52 let device_name = tranquil_signal::DeviceName::new("tranquil-pds".to_string()) 54 53 .map_err(|e| ApiError::InternalError(Some(format!("invalid device name: {e}"))))?; 55 54 56 - let link_result = tranquil_signal::SignalClient::link_device( 57 - &state.repos.pool, 58 - device_name, 59 - state.shutdown.clone(), 60 - link_cancel, 61 - slot.linking_flag(), 62 - ) 63 - .await 64 - .map_err(|e| ApiError::InternalError(Some(format!("Signal linking failed: {e}"))))?; 55 + let signal_store = state 56 + .signal_store_provider 57 + .as_ref() 58 + .ok_or_else(|| ApiError::InternalError(Some("Signal store not configured".into())))?; 59 + 60 + let link_result = signal_store 61 + .link_signal_device( 62 + device_name, 63 + state.shutdown.clone(), 64 + link_cancel, 65 + slot.linking_flag(), 66 + ) 67 + .await 68 + .map_err(|e| ApiError::InternalError(Some(format!("Signal linking failed: {e}"))))?; 65 69 66 70 let qr_base64 = url_to_qr_png_base64(link_result.url.as_str()) 67 71 .map_err(|e| ApiError::InternalError(Some(format!("QR generation failed: {e}"))))?; ··· 108 112 .as_ref() 109 113 .ok_or_else(|| ApiError::InvalidRequest("Signal is not enabled".into()))?; 110 114 111 - let store = PgSignalStore::new(state.repos.pool.clone()); 112 - store 113 - .clear_all() 115 + let signal_store = state 116 + .signal_store_provider 117 + .as_ref() 118 + .ok_or_else(|| ApiError::InternalError(Some("Signal store not configured".into())))?; 119 + 120 + signal_store 121 + .clear_signal_data() 114 122 .await 115 123 .map_err(|e| ApiError::InternalError(Some(format!("Failed to clear signal data: {e}"))))?; 116 124
-17
crates/tranquil-api/src/delegation.rs
··· 18 18 use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited}; 19 19 use tranquil_pds::state::AppState; 20 20 use tranquil_pds::types::{Did, Handle}; 21 - use tranquil_types::CidLink; 22 21 23 22 pub async fn list_controllers( 24 23 State(state): State<AppState>, ··· 419 418 return Err(ApiError::InternalError(None)); 420 419 } 421 420 }; 422 - 423 - state 424 - .repos 425 - .repo 426 - .create_repo( 427 - user_id, 428 - &did, 429 - &handle, 430 - &CidLink::from(&repo.commit_cid), 431 - &repo.repo_rev, 432 - ) 433 - .await 434 - .map_err(|e| { 435 - error!("failed to register repo in backend: {e:?}"); 436 - ApiError::InternalError(None) 437 - })?; 438 421 439 422 if let Some(validated) = validated_invite_code 440 423 && let Err(e) = state
-17
crates/tranquil-api/src/identity/account.rs
··· 15 15 use tranquil_pds::state::AppState; 16 16 use tranquil_pds::types::{Did, Handle, PlainPassword}; 17 17 use tranquil_pds::validation::validate_password; 18 - use tranquil_types::CidLink; 19 - 20 18 #[derive(Deserialize)] 21 19 #[serde(rename_all = "camelCase")] 22 20 pub struct CreateAccountInput { ··· 548 546 } 549 547 }; 550 548 let user_id = create_result.user_id; 551 - if let Err(e) = state 552 - .repos 553 - .repo 554 - .create_repo( 555 - user_id, 556 - &did_for_commit, 557 - &handle_typed, 558 - &CidLink::from(&repo.commit_cid), 559 - &repo.repo_rev, 560 - ) 561 - .await 562 - { 563 - error!("failed to register repo in backend: {e:?}"); 564 - return ApiError::InternalError(None).into_response(); 565 - } 566 549 if !is_migration && !is_did_web_byod { 567 550 super::provision::sequence_new_account( 568 551 &state,
-17
crates/tranquil-api/src/server/passkey_account.rs
··· 15 15 use tranquil_pds::state::AppState; 16 16 use tranquil_pds::types::{Did, Handle, PlainPassword}; 17 17 use tranquil_pds::validation::validate_password; 18 - use tranquil_types::CidLink; 19 18 20 19 fn generate_setup_token() -> String { 21 20 let mut rng = rand::thread_rng(); ··· 366 365 } 367 366 }; 368 367 let user_id = create_result.user_id; 369 - 370 - state 371 - .repos 372 - .repo 373 - .create_repo( 374 - user_id, 375 - &did_typed, 376 - &handle_typed, 377 - &CidLink::from(&repo.commit_cid), 378 - &repo.repo_rev, 379 - ) 380 - .await 381 - .map_err(|e| { 382 - error!("failed to register repo in backend: {e:?}"); 383 - ApiError::InternalError(None) 384 - })?; 385 368 386 369 if !is_byod_did_web { 387 370 crate::identity::provision::sequence_new_account(
+64
crates/tranquil-db-traits/src/infra.rs
··· 186 186 } 187 187 188 188 #[derive(Debug, Clone)] 189 + pub struct ReservedSigningKeyFull { 190 + pub id: Uuid, 191 + pub did: Option<Did>, 192 + pub public_key_did_key: String, 193 + pub private_key_bytes: Vec<u8>, 194 + pub expires_at: DateTime<Utc>, 195 + pub used_at: Option<DateTime<Utc>>, 196 + } 197 + 198 + #[derive(Debug, Clone)] 189 199 pub struct DeletionRequest { 190 200 pub did: Did, 191 201 pub expires_at: DateTime<Utc>, 202 + } 203 + 204 + #[derive(Debug, Clone)] 205 + pub struct DeletionRequestWithToken { 206 + pub token: String, 207 + pub did: Did, 208 + pub expires_at: DateTime<Utc>, 209 + } 210 + 211 + #[derive(Debug, Clone)] 212 + pub struct PlcTokenInfo { 213 + pub token: String, 214 + pub expires_at: DateTime<Utc>, 215 + } 216 + 217 + #[derive(Debug, Clone)] 218 + pub struct PasswordResetInfo { 219 + pub code: Option<String>, 220 + pub expires_at: Option<DateTime<Utc>>, 192 221 } 193 222 194 223 #[async_trait] ··· 406 435 &self, 407 436 user_ids: &[Uuid], 408 437 ) -> Result<Vec<(Uuid, String)>, DbError>; 438 + 439 + async fn get_deletion_request_by_did( 440 + &self, 441 + did: &Did, 442 + ) -> Result<Option<DeletionRequestWithToken>, DbError>; 443 + 444 + async fn get_latest_comms_for_user( 445 + &self, 446 + user_id: Uuid, 447 + comms_type: CommsType, 448 + limit: i64, 449 + ) -> Result<Vec<QueuedComms>, DbError>; 450 + 451 + async fn count_comms_by_type( 452 + &self, 453 + user_id: Uuid, 454 + comms_type: CommsType, 455 + ) -> Result<i64, DbError>; 456 + 457 + async fn delete_comms_by_type_for_user( 458 + &self, 459 + user_id: Uuid, 460 + comms_type: CommsType, 461 + ) -> Result<u64, DbError>; 462 + 463 + async fn expire_deletion_request(&self, token: &str) -> Result<(), DbError>; 464 + 465 + async fn get_reserved_signing_key_full( 466 + &self, 467 + public_key_did_key: &str, 468 + ) -> Result<Option<ReservedSigningKeyFull>, DbError>; 469 + 470 + async fn get_plc_tokens_by_did(&self, did: &Did) -> Result<Vec<PlcTokenInfo>, DbError>; 471 + 472 + async fn count_plc_tokens_by_did(&self, did: &Did) -> Result<i64, DbError>; 409 473 } 410 474 411 475 #[derive(Debug, Clone)]
+4 -3
crates/tranquil-db-traits/src/lib.rs
··· 22 22 }; 23 23 pub use error::DbError; 24 24 pub use infra::{ 25 - AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest, InfraRepository, 26 - InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, InviteCodeState, InviteCodeUse, 27 - NotificationHistoryRow, QueuedComms, ReservedSigningKey, 25 + AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest, 26 + DeletionRequestWithToken, InfraRepository, InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, 27 + InviteCodeState, InviteCodeUse, NotificationHistoryRow, PasswordResetInfo, PlcTokenInfo, 28 + QueuedComms, ReservedSigningKey, ReservedSigningKeyFull, 28 29 }; 29 30 pub use invite_code::{InviteCodeError, ValidatedInviteCode}; 30 31 pub use oauth::{
+5
crates/tranquil-db-traits/src/oauth.rs
··· 324 324 did: &Did, 325 325 except_token_id: &TokenId, 326 326 ) -> Result<u64, DbError>; 327 + 328 + async fn get_2fa_challenge_code( 329 + &self, 330 + request_uri: &RequestId, 331 + ) -> Result<Option<String>, DbError>; 327 332 }
+14
crates/tranquil-db-traits/src/user.rs
··· 223 223 224 224 async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError>; 225 225 226 + async fn set_admin_status(&self, did: &Did, is_admin: bool) -> Result<(), DbError>; 227 + 226 228 async fn get_notification_prefs(&self, did: &Did) 227 229 -> Result<Option<NotificationPrefs>, DbError>; 228 230 ··· 584 586 &self, 585 587 input: &RecoverPasskeyAccountInput, 586 588 ) -> Result<RecoverPasskeyAccountResult, DbError>; 589 + 590 + async fn get_password_reset_info( 591 + &self, 592 + email: &str, 593 + ) -> Result<Option<crate::PasswordResetInfo>, DbError>; 594 + 595 + async fn enable_totp_verified(&self, did: &Did, encrypted_secret: &[u8]) 596 + -> Result<(), DbError>; 597 + 598 + async fn set_two_factor_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError>; 599 + 600 + async fn expire_password_reset_code(&self, email: &str) -> Result<(), DbError>; 587 601 } 588 602 589 603 #[derive(Debug, Clone)]
+158 -3
crates/tranquil-db/src/postgres/infra.rs
··· 3 3 use sqlx::PgPool; 4 4 use tranquil_db_traits::{ 5 5 AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DbError, DeletionRequest, 6 - InfraRepository, InviteCodeError, InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, 7 - InviteCodeState, InviteCodeUse, NotificationHistoryRow, QueuedComms, ReservedSigningKey, 8 - ValidatedInviteCode, 6 + DeletionRequestWithToken, InfraRepository, InviteCodeError, InviteCodeInfo, InviteCodeRow, 7 + InviteCodeSortOrder, InviteCodeState, InviteCodeUse, NotificationHistoryRow, PlcTokenInfo, 8 + QueuedComms, ReservedSigningKey, ReservedSigningKeyFull, ValidatedInviteCode, 9 9 }; 10 10 use tranquil_types::{CidLink, Did, Handle}; 11 11 use uuid::Uuid; ··· 1033 1033 .into_iter() 1034 1034 .map(|r| (r.used_by_user, r.code)) 1035 1035 .collect()) 1036 + } 1037 + 1038 + async fn get_deletion_request_by_did( 1039 + &self, 1040 + did: &Did, 1041 + ) -> Result<Option<DeletionRequestWithToken>, DbError> { 1042 + let row = sqlx::query!( 1043 + r#"SELECT token, did, expires_at FROM account_deletion_requests WHERE did = $1"#, 1044 + did.as_str() 1045 + ) 1046 + .fetch_optional(&self.pool) 1047 + .await 1048 + .map_err(map_sqlx_error)?; 1049 + 1050 + Ok(row.map(|r| DeletionRequestWithToken { 1051 + token: r.token, 1052 + did: Did::new(r.did).expect("valid DID in database"), 1053 + expires_at: r.expires_at, 1054 + })) 1055 + } 1056 + 1057 + async fn get_latest_comms_for_user( 1058 + &self, 1059 + user_id: Uuid, 1060 + comms_type: CommsType, 1061 + limit: i64, 1062 + ) -> Result<Vec<QueuedComms>, DbError> { 1063 + let results = sqlx::query_as!( 1064 + QueuedComms, 1065 + r#"SELECT 1066 + id, user_id, 1067 + channel as "channel: CommsChannel", 1068 + comms_type as "comms_type: CommsType", 1069 + status as "status: CommsStatus", 1070 + recipient, subject, body, metadata, 1071 + attempts, max_attempts, last_error, 1072 + created_at, updated_at, scheduled_for, processed_at 1073 + FROM comms_queue 1074 + WHERE user_id = $1 AND comms_type = $2 1075 + ORDER BY created_at DESC 1076 + LIMIT $3"#, 1077 + user_id, 1078 + comms_type as CommsType, 1079 + limit 1080 + ) 1081 + .fetch_all(&self.pool) 1082 + .await 1083 + .map_err(map_sqlx_error)?; 1084 + 1085 + Ok(results) 1086 + } 1087 + 1088 + async fn count_comms_by_type( 1089 + &self, 1090 + user_id: Uuid, 1091 + comms_type: CommsType, 1092 + ) -> Result<i64, DbError> { 1093 + let count = sqlx::query_scalar!( 1094 + r#"SELECT COUNT(*) as "count!" FROM comms_queue WHERE user_id = $1 AND comms_type = $2"#, 1095 + user_id, 1096 + comms_type as CommsType 1097 + ) 1098 + .fetch_one(&self.pool) 1099 + .await 1100 + .map_err(map_sqlx_error)?; 1101 + 1102 + Ok(count) 1103 + } 1104 + 1105 + async fn delete_comms_by_type_for_user( 1106 + &self, 1107 + user_id: Uuid, 1108 + comms_type: CommsType, 1109 + ) -> Result<u64, DbError> { 1110 + let result = sqlx::query!( 1111 + "DELETE FROM comms_queue WHERE user_id = $1 AND comms_type = $2", 1112 + user_id, 1113 + comms_type as CommsType 1114 + ) 1115 + .execute(&self.pool) 1116 + .await 1117 + .map_err(map_sqlx_error)?; 1118 + 1119 + Ok(result.rows_affected()) 1120 + } 1121 + 1122 + async fn expire_deletion_request(&self, token: &str) -> Result<(), DbError> { 1123 + sqlx::query!( 1124 + "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1", 1125 + token 1126 + ) 1127 + .execute(&self.pool) 1128 + .await 1129 + .map_err(map_sqlx_error)?; 1130 + 1131 + Ok(()) 1132 + } 1133 + 1134 + async fn get_reserved_signing_key_full( 1135 + &self, 1136 + public_key_did_key: &str, 1137 + ) -> Result<Option<ReservedSigningKeyFull>, DbError> { 1138 + let row = sqlx::query!( 1139 + r#"SELECT id, did, public_key_did_key, private_key_bytes, expires_at, used_at 1140 + FROM reserved_signing_keys WHERE public_key_did_key = $1"#, 1141 + public_key_did_key 1142 + ) 1143 + .fetch_optional(&self.pool) 1144 + .await 1145 + .map_err(map_sqlx_error)?; 1146 + 1147 + Ok(row.map(|r| ReservedSigningKeyFull { 1148 + id: r.id, 1149 + did: r.did.map(|d| Did::new(d).expect("valid DID in database")), 1150 + public_key_did_key: r.public_key_did_key, 1151 + private_key_bytes: r.private_key_bytes, 1152 + expires_at: r.expires_at, 1153 + used_at: r.used_at, 1154 + })) 1155 + } 1156 + 1157 + async fn get_plc_tokens_by_did(&self, did: &Did) -> Result<Vec<PlcTokenInfo>, DbError> { 1158 + let results = sqlx::query!( 1159 + r#"SELECT t.token, t.expires_at 1160 + FROM plc_operation_tokens t 1161 + JOIN users u ON t.user_id = u.id 1162 + WHERE u.did = $1"#, 1163 + did.as_str() 1164 + ) 1165 + .fetch_all(&self.pool) 1166 + .await 1167 + .map_err(map_sqlx_error)?; 1168 + 1169 + Ok(results 1170 + .into_iter() 1171 + .map(|r| PlcTokenInfo { 1172 + token: r.token, 1173 + expires_at: r.expires_at, 1174 + }) 1175 + .collect()) 1176 + } 1177 + 1178 + async fn count_plc_tokens_by_did(&self, did: &Did) -> Result<i64, DbError> { 1179 + let count = sqlx::query_scalar!( 1180 + r#"SELECT COUNT(*) as "count!" 1181 + FROM plc_operation_tokens t 1182 + JOIN users u ON t.user_id = u.id 1183 + WHERE u.did = $1"#, 1184 + did.as_str() 1185 + ) 1186 + .fetch_one(&self.pool) 1187 + .await 1188 + .map_err(map_sqlx_error)?; 1189 + 1190 + Ok(count) 1036 1191 } 1037 1192 }
+2 -2
crates/tranquil-db/src/postgres/mod.rs
··· 28 28 pub use user::PostgresUserRepository; 29 29 30 30 pub struct PostgresRepositories { 31 - pub pool: PgPool, 31 + pub pool: Option<PgPool>, 32 32 pub user: Arc<dyn UserRepository>, 33 33 pub oauth: Arc<dyn OAuthRepository>, 34 34 pub session: Arc<dyn SessionRepository>, ··· 44 44 impl PostgresRepositories { 45 45 pub fn new(pool: PgPool) -> Self { 46 46 Self { 47 - pool: pool.clone(), 47 + pool: Some(pool.clone()), 48 48 user: Arc::new(PostgresUserRepository::new(pool.clone())), 49 49 oauth: Arc::new(PostgresOAuthRepository::new(pool.clone())), 50 50 session: Arc::new(PostgresSessionRepository::new(pool.clone())),
+15
crates/tranquil-db/src/postgres/oauth.rs
··· 1323 1323 .map_err(map_sqlx_error)?; 1324 1324 Ok(result.rows_affected()) 1325 1325 } 1326 + 1327 + async fn get_2fa_challenge_code( 1328 + &self, 1329 + request_uri: &RequestId, 1330 + ) -> Result<Option<String>, DbError> { 1331 + let code = sqlx::query_scalar!( 1332 + "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1", 1333 + request_uri.as_str() 1334 + ) 1335 + .fetch_optional(&self.pool) 1336 + .await 1337 + .map_err(map_sqlx_error)?; 1338 + 1339 + Ok(code) 1340 + } 1326 1341 }
+78 -2
crates/tranquil-db/src/postgres/user.rs
··· 234 234 limit: i64, 235 235 ) -> Result<Vec<AccountSearchResult>, DbError> { 236 236 let cursor_str = cursor_did.map(|d| d.as_str()); 237 + let email_like = email_filter.map(|e| format!("%{e}%")); 238 + let handle_like = handle_filter.map(|h| format!("%{h}%")); 237 239 let rows = sqlx::query!( 238 240 r#"SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled 239 241 FROM users ··· 243 245 ORDER BY did ASC 244 246 LIMIT $4"#, 245 247 cursor_str, 246 - email_filter, 247 - handle_filter, 248 + email_like.as_deref(), 249 + handle_like.as_deref(), 248 250 limit 249 251 ) 250 252 .fetch_all(&self.pool) ··· 625 627 .await 626 628 .map_err(map_sqlx_error)?; 627 629 Ok(result.rows_affected()) 630 + } 631 + 632 + async fn set_admin_status(&self, did: &Did, is_admin: bool) -> Result<(), DbError> { 633 + sqlx::query!( 634 + "UPDATE users SET is_admin = $1 WHERE did = $2", 635 + is_admin, 636 + did.as_str() 637 + ) 638 + .execute(&self.pool) 639 + .await 640 + .map_err(map_sqlx_error)?; 641 + Ok(()) 628 642 } 629 643 630 644 async fn get_notification_prefs( ··· 3305 3319 .await 3306 3320 .map_err(map_sqlx_error)?; 3307 3321 Ok(row.flatten()) 3322 + } 3323 + 3324 + async fn get_password_reset_info( 3325 + &self, 3326 + email: &str, 3327 + ) -> Result<Option<tranquil_db_traits::PasswordResetInfo>, DbError> { 3328 + let row = sqlx::query!( 3329 + "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1", 3330 + email 3331 + ) 3332 + .fetch_optional(&self.pool) 3333 + .await 3334 + .map_err(map_sqlx_error)?; 3335 + 3336 + Ok(row.map(|r| tranquil_db_traits::PasswordResetInfo { 3337 + code: r.password_reset_code, 3338 + expires_at: r.password_reset_code_expires_at, 3339 + })) 3340 + } 3341 + 3342 + async fn enable_totp_verified( 3343 + &self, 3344 + did: &Did, 3345 + encrypted_secret: &[u8], 3346 + ) -> Result<(), DbError> { 3347 + sqlx::query!( 3348 + r#"INSERT INTO user_totp (did, secret_encrypted, encryption_version, verified, created_at) 3349 + VALUES ($1, $2, 1, TRUE, NOW()) 3350 + ON CONFLICT (did) DO UPDATE SET secret_encrypted = $2, verified = TRUE"#, 3351 + did.as_str(), 3352 + encrypted_secret 3353 + ) 3354 + .execute(&self.pool) 3355 + .await 3356 + .map_err(map_sqlx_error)?; 3357 + 3358 + Ok(()) 3359 + } 3360 + 3361 + async fn set_two_factor_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError> { 3362 + sqlx::query!( 3363 + "UPDATE users SET two_factor_enabled = $1 WHERE did = $2", 3364 + enabled, 3365 + did.as_str() 3366 + ) 3367 + .execute(&self.pool) 3368 + .await 3369 + .map_err(map_sqlx_error)?; 3370 + 3371 + Ok(()) 3372 + } 3373 + 3374 + async fn expire_password_reset_code(&self, email: &str) -> Result<(), DbError> { 3375 + sqlx::query!( 3376 + "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1", 3377 + email 3378 + ) 3379 + .execute(&self.pool) 3380 + .await 3381 + .map_err(map_sqlx_error)?; 3382 + 3383 + Ok(()) 3308 3384 } 3309 3385 }
+1
crates/tranquil-pds/Cargo.toml
··· 99 99 tranquil-sync = { workspace = true } 100 100 tranquil-api = { workspace = true } 101 101 tranquil-oauth-server = { workspace = true } 102 + tracing-subscriber = { workspace = true, features = ["env-filter"] } 102 103 wiremock = { workspace = true }
+135 -69
crates/tranquil-pds/src/state.rs
··· 48 48 pub shutdown: CancellationToken, 49 49 pub bootstrap_invite_code: Option<String>, 50 50 pub signal_sender: Option<Arc<tranquil_signal::SignalSlot>>, 51 + pub signal_store_provider: Option<Arc<dyn tranquil_signal::SignalStoreProvider>>, 51 52 } 52 53 53 54 #[derive(Debug, Clone, Copy)] ··· 210 211 211 212 pub async fn new(shutdown: CancellationToken) -> Result<Self, Box<dyn Error>> { 212 213 let cfg = tranquil_config::get(); 213 - let database_url = &cfg.database.url; 214 - let max_connections = cfg.database.max_connections; 215 - let min_connections = cfg.database.min_connections; 216 - let acquire_timeout_secs = cfg.database.acquire_timeout_secs; 217 214 218 - tracing::info!( 219 - "Configuring database pool: max={}, min={}, acquire_timeout={}s", 220 - max_connections, 221 - min_connections, 222 - acquire_timeout_secs 223 - ); 215 + match cfg.storage.repo_backend() { 216 + tranquil_config::RepoBackend::TranquilStore => { 217 + tracing::info!( 218 + "tranquil-store repo backend active. EXPERIMENTAL! No garbage collection, no backup/restore" 219 + ); 220 + Ok(Self::from_store(shutdown).await) 221 + } 222 + tranquil_config::RepoBackend::Postgres => { 223 + let database_url = &cfg.database.url; 224 + let max_connections = cfg.database.max_connections; 225 + let min_connections = cfg.database.min_connections; 226 + let acquire_timeout_secs = cfg.database.acquire_timeout_secs; 227 + 228 + tracing::info!( 229 + "Configuring database pool: max={}, min={}, acquire_timeout={}s", 230 + max_connections, 231 + min_connections, 232 + acquire_timeout_secs 233 + ); 234 + 235 + let db = sqlx::postgres::PgPoolOptions::new() 236 + .max_connections(max_connections) 237 + .min_connections(min_connections) 238 + .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs)) 239 + .idle_timeout(std::time::Duration::from_secs(300)) 240 + .max_lifetime(std::time::Duration::from_secs(1800)) 241 + .connect(database_url) 242 + .await 243 + .map_err(|e| format!("Failed to connect to Postgres: {}", e))?; 224 244 225 - let db = sqlx::postgres::PgPoolOptions::new() 226 - .max_connections(max_connections) 227 - .min_connections(min_connections) 228 - .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs)) 229 - .idle_timeout(std::time::Duration::from_secs(300)) 230 - .max_lifetime(std::time::Duration::from_secs(1800)) 231 - .connect(database_url) 232 - .await 233 - .map_err(|e| format!("Failed to connect to Postgres: {}", e))?; 245 + sqlx::migrate!("./migrations") 246 + .run(&db) 247 + .await 248 + .map_err(|e| format!("Failed to run migrations: {}", e))?; 234 249 235 - sqlx::migrate!("./migrations") 236 - .run(&db) 237 - .await 238 - .map_err(|e| format!("Failed to run migrations: {}", e))?; 250 + let bootstrap_invite_code = match ( 251 + cfg.server.invite_code_required, 252 + sqlx::query_scalar!("SELECT COUNT(*) FROM users") 253 + .fetch_one(&db) 254 + .await, 255 + ) { 256 + (true, Ok(Some(0))) => { 257 + let code = crate::util::gen_invite_code(); 258 + tracing::info!( 259 + "No users exist and invite codes are required. Bootstrap invite code: {}", 260 + code 261 + ); 262 + Some(code) 263 + } 264 + _ => None, 265 + }; 239 266 240 - let bootstrap_invite_code = match ( 241 - cfg.server.invite_code_required, 242 - sqlx::query_scalar!("SELECT COUNT(*) FROM users") 243 - .fetch_one(&db) 244 - .await, 245 - ) { 246 - (true, Ok(Some(0))) => { 247 - let code = crate::util::gen_invite_code(); 248 - tracing::info!( 249 - "No users exist and invite codes are required. Bootstrap invite code: {}", 250 - code 251 - ); 252 - Some(code) 267 + let mut state = Self::from_db(db, shutdown).await; 268 + state.bootstrap_invite_code = bootstrap_invite_code; 269 + Ok(state) 253 270 } 254 - _ => None, 271 + } 272 + } 273 + 274 + pub async fn from_db(db: PgPool, shutdown: CancellationToken) -> Self { 275 + let cfg = tranquil_config::get(); 276 + let (repos, block_store, signal_store_provider): ( 277 + PostgresRepositories, 278 + crate::repo::AnyBlockStore, 279 + Option<Arc<dyn tranquil_signal::SignalStoreProvider>>, 280 + ) = match cfg.storage.repo_backend() == tranquil_config::RepoBackend::TranquilStore { 281 + true => { 282 + let wiring = wire_tranquil_store(&cfg.tranquil_store, shutdown.clone()); 283 + ( 284 + wiring.repos, 285 + crate::repo::AnyBlockStore::TranquilStore(wiring.blockstore), 286 + Some(wiring.signal_provider), 287 + ) 288 + } 289 + false => { 290 + let repos = PostgresRepositories::new(db.clone()); 291 + let provider: Arc<dyn tranquil_signal::SignalStoreProvider> = 292 + Arc::new(tranquil_signal::PgSignalStoreProvider { pool: db.clone() }); 293 + ( 294 + repos, 295 + crate::repo::AnyBlockStore::Postgres(PostgresBlockStore::new(db)), 296 + Some(provider), 297 + ) 298 + } 255 299 }; 256 300 257 - let mut state = Self::from_db(db, shutdown).await; 258 - state.bootstrap_invite_code = bootstrap_invite_code; 259 - Ok(state) 301 + Self::build(repos, block_store, signal_store_provider, shutdown).await 302 + } 303 + 304 + pub async fn from_store(shutdown: CancellationToken) -> Self { 305 + let cfg = tranquil_config::get(); 306 + let wiring = wire_tranquil_store(&cfg.tranquil_store, shutdown.clone()); 307 + 308 + Self::build( 309 + wiring.repos, 310 + crate::repo::AnyBlockStore::TranquilStore(wiring.blockstore), 311 + Some(wiring.signal_provider), 312 + shutdown, 313 + ) 314 + .await 260 315 } 261 316 262 - pub async fn from_db(db: PgPool, shutdown: CancellationToken) -> Self { 317 + async fn build( 318 + repos: PostgresRepositories, 319 + block_store: crate::repo::AnyBlockStore, 320 + signal_store_provider: Option<Arc<dyn tranquil_signal::SignalStoreProvider>>, 321 + shutdown: CancellationToken, 322 + ) -> Self { 263 323 AuthConfig::init(); 264 324 init_rate_limit_override(); 265 - 266 - let mut repos = PostgresRepositories::new(db.clone()); 267 325 268 326 let cfg = tranquil_config::get(); 269 - let block_store = 270 - match cfg.storage.repo_backend() == tranquil_config::RepoBackend::TranquilStore { 271 - true => { 272 - let bs = wire_tranquil_store(&mut repos, &cfg.tranquil_store, shutdown.clone()); 273 - crate::repo::AnyBlockStore::TranquilStore(bs) 274 - } 275 - false => crate::repo::AnyBlockStore::Postgres(PostgresBlockStore::new(db)), 276 - }; 277 - 278 327 let repos = Arc::new(repos); 279 328 let blob_store = create_blob_storage().await; 280 - 281 - let firehose_buffer_size = tranquil_config::get().firehose.buffer_size; 282 - 329 + let firehose_buffer_size = cfg.firehose.buffer_size; 283 330 let (firehose_tx, _) = broadcast::channel(firehose_buffer_size); 284 331 let rate_limiters = Arc::new(RateLimiters::new()); 285 332 let repo_write_locks = Arc::new(RepoWriteLocks::new()); ··· 290 337 let sso_config = SsoConfig::init(); 291 338 let sso_manager = SsoManager::from_config(sso_config); 292 339 let webauthn_config = Arc::new( 293 - WebAuthnConfig::new(&tranquil_config::get().server.hostname) 340 + WebAuthnConfig::new(&cfg.server.hostname) 294 341 .expect("Failed to create WebAuthn config at startup"), 295 342 ); 296 343 ··· 311 358 shutdown, 312 359 bootstrap_invite_code: None, 313 360 signal_sender: None, 361 + signal_store_provider, 314 362 } 315 363 } 316 364 ··· 388 436 389 437 true 390 438 } 439 + } 440 + 441 + struct TranquilStoreWiring { 442 + blockstore: tranquil_store::blockstore::TranquilBlockStore, 443 + signal_provider: Arc<dyn tranquil_signal::SignalStoreProvider>, 444 + repos: PostgresRepositories, 391 445 } 392 446 393 447 fn wire_tranquil_store( 394 - repos: &mut PostgresRepositories, 395 448 store_cfg: &tranquil_config::TranquilStoreConfig, 396 449 shutdown: CancellationToken, 397 - ) -> tranquil_store::blockstore::TranquilBlockStore { 450 + ) -> TranquilStoreWiring { 398 451 use tranquil_store::RealIO; 399 452 use tranquil_store::blockstore::{BlockStoreConfig, TranquilBlockStore}; 400 453 use tranquil_store::eventlog::{EventLog, EventLogBridge, EventLogConfig}; ··· 460 513 } 461 514 462 515 let notifier = bridge.notifier(); 516 + let signal_db = metastore.database().clone(); 517 + let signal_ks = metastore.signal_keyspace(); 463 518 464 519 let pool = Arc::new(HandlerPool::spawn::<RealIO>( 465 520 metastore, ··· 480 535 481 536 tracing::info!(data_dir = %store_cfg.data_dir, "tranquil-store data directory"); 482 537 483 - repos.repo = Arc::new(client.clone()); 484 - repos.backlink = Arc::new(client.clone()); 485 - repos.blob = Arc::new(client.clone()); 486 - repos.user = Arc::new(client.clone()); 487 - repos.session = Arc::new(client.clone()); 488 - repos.oauth = Arc::new(client.clone()); 489 - repos.infra = Arc::new(client.clone()); 490 - repos.delegation = Arc::new(client.clone()); 491 - repos.sso = Arc::new(client); 492 - repos.event_notifier = Arc::new(notifier); 538 + let repos = PostgresRepositories { 539 + pool: None, 540 + repo: Arc::new(client.clone()), 541 + backlink: Arc::new(client.clone()), 542 + blob: Arc::new(client.clone()), 543 + user: Arc::new(client.clone()), 544 + session: Arc::new(client.clone()), 545 + oauth: Arc::new(client.clone()), 546 + infra: Arc::new(client.clone()), 547 + delegation: Arc::new(client.clone()), 548 + sso: Arc::new(client), 549 + event_notifier: Arc::new(notifier), 550 + }; 551 + 552 + let signal_provider: Arc<dyn tranquil_signal::SignalStoreProvider> = Arc::new( 553 + tranquil_signal::fjall_store::FjallSignalStoreProvider::new(signal_db, signal_ks), 554 + ); 493 555 494 - blockstore 556 + TranquilStoreWiring { 557 + blockstore, 558 + signal_provider, 559 + repos, 560 + } 495 561 }
+35 -27
crates/tranquil-pds/tests/account_notifications.rs
··· 1 1 mod common; 2 - use common::{base_url, client, create_account_and_login, get_test_db_pool}; 2 + use common::{base_url, client, create_account_and_login, get_test_repos}; 3 3 use serde_json::{Value, json}; 4 + use tranquil_db_traits::{CommsChannel, CommsType}; 5 + use tranquil_types::Did; 4 6 5 7 #[tokio::test] 6 8 async fn test_get_notification_history() { 7 9 let client = client(); 8 10 let base = base_url().await; 9 - let pool = get_test_db_pool().await; 11 + let repos = get_test_repos().await; 10 12 let (token, did) = create_account_and_login(&client).await; 11 13 12 - let user_id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE did = $1") 13 - .bind(&did) 14 - .fetch_one(pool) 14 + let user_id = repos 15 + .user 16 + .get_id_by_did(&Did::new(did).unwrap()) 15 17 .await 18 + .expect("DB error") 16 19 .expect("User not found"); 17 20 18 21 for i in 0..3 { 19 - sqlx::query( 20 - r#"INSERT INTO comms_queue (user_id, channel, comms_type, recipient, subject, body) 21 - VALUES ($1, 'email', 'welcome', $2, $3, $4)"#, 22 - ) 23 - .bind(user_id) 24 - .bind("test@example.com") 25 - .bind(format!("Subject {}", i)) 26 - .bind(format!("Body {}", i)) 27 - .execute(pool) 28 - .await 29 - .expect("Failed to enqueue"); 22 + repos 23 + .infra 24 + .enqueue_comms( 25 + Some(user_id), 26 + CommsChannel::Email, 27 + CommsType::Welcome, 28 + "test@example.com", 29 + Some(&format!("Subject {}", i)), 30 + &format!("Body {}", i), 31 + None, 32 + ) 33 + .await 34 + .expect("Failed to enqueue"); 30 35 } 31 36 32 37 let resp = client ··· 140 145 async fn test_update_email_via_notification_prefs() { 141 146 let client = client(); 142 147 let base = base_url().await; 143 - let pool = get_test_db_pool().await; 148 + let repos = get_test_repos().await; 144 149 let (token, did) = create_account_and_login(&client).await; 145 150 146 151 let unique_email = format!("newemail_{}@example.com", uuid::Uuid::new_v4()); ··· 163 168 .contains(&json!("email")) 164 169 ); 165 170 166 - let user_id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE did = $1") 167 - .bind(&did) 168 - .fetch_one(pool) 171 + let user_id = repos 172 + .user 173 + .get_id_by_did(&Did::new(did).unwrap()) 169 174 .await 175 + .expect("DB error") 170 176 .expect("User not found"); 171 177 172 - let body_text: String = sqlx::query_scalar( 173 - "SELECT body FROM comms_queue WHERE user_id = $1 AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1", 174 - ) 175 - .bind(user_id) 176 - .fetch_one(pool) 177 - .await 178 - .expect("Verification code not found"); 178 + let comms = repos 179 + .infra 180 + .get_latest_comms_for_user(user_id, CommsType::EmailUpdate, 1) 181 + .await 182 + .expect("DB error"); 183 + let body_text = comms 184 + .first() 185 + .map(|c| c.body.clone()) 186 + .expect("Verification code not found"); 179 187 180 188 let code = body_text 181 189 .lines()
+34 -21
crates/tranquil-pds/tests/admin_email.rs
··· 2 2 3 3 use reqwest::StatusCode; 4 4 use serde_json::{Value, json}; 5 + use tranquil_db_traits::CommsType; 6 + use tranquil_types::Did; 5 7 6 8 #[tokio::test] 7 9 async fn test_send_email_success() { 8 10 let client = common::client(); 9 11 let base_url = common::base_url().await; 10 - let pool = common::get_test_db_pool().await; 12 + let repos = common::get_test_repos().await; 11 13 let (access_jwt, did) = common::create_admin_account_and_login(&client).await; 12 14 let res = client 13 15 .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) ··· 24 26 assert_eq!(res.status(), StatusCode::OK); 25 27 let body: Value = res.json().await.expect("Invalid JSON"); 26 28 assert_eq!(body["sent"], true); 27 - let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 28 - .fetch_one(pool) 29 + let user_id = repos 30 + .user 31 + .get_id_by_did(&Did::new(did).unwrap()) 29 32 .await 33 + .expect("DB error") 30 34 .expect("User not found"); 31 - let notification = sqlx::query!( 32 - "SELECT subject, body, comms_type as \"comms_type: String\" FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' ORDER BY created_at DESC LIMIT 1", 33 - user.id 34 - ) 35 - .fetch_one(pool) 36 - .await 37 - .expect("Notification not found"); 35 + let comms = repos 36 + .infra 37 + .get_latest_comms_for_user(user_id, CommsType::AdminEmail, 1) 38 + .await 39 + .expect("DB error"); 40 + let notification = comms.first().expect("Notification not found"); 38 41 assert_eq!(notification.subject.as_deref(), Some("Test Admin Email")); 39 42 assert!( 40 43 notification ··· 47 50 async fn test_send_email_default_subject() { 48 51 let client = common::client(); 49 52 let base_url = common::base_url().await; 50 - let pool = common::get_test_db_pool().await; 53 + let repos = common::get_test_repos().await; 51 54 let (access_jwt, did) = common::create_admin_account_and_login(&client).await; 52 55 let res = client 53 56 .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) ··· 63 66 assert_eq!(res.status(), StatusCode::OK); 64 67 let body: Value = res.json().await.expect("Invalid JSON"); 65 68 assert_eq!(body["sent"], true); 66 - let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 67 - .fetch_one(pool) 69 + let user_id = repos 70 + .user 71 + .get_id_by_did(&Did::new(did).unwrap()) 68 72 .await 73 + .expect("DB error") 69 74 .expect("User not found"); 70 - let notification = sqlx::query!( 71 - "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1", 72 - user.id 73 - ) 74 - .fetch_one(pool) 75 - .await 76 - .expect("Notification not found"); 75 + let comms = repos 76 + .infra 77 + .get_latest_comms_for_user(user_id, CommsType::AdminEmail, 10) 78 + .await 79 + .expect("DB error"); 80 + let notification = comms 81 + .iter() 82 + .find(|c| c.body == "Email without subject") 83 + .expect("Notification not found"); 77 84 assert!(notification.subject.is_some()); 78 - assert!(notification.subject.unwrap().contains("Message from")); 85 + assert!( 86 + notification 87 + .subject 88 + .as_ref() 89 + .unwrap() 90 + .contains("Message from") 91 + ); 79 92 } 80 93 81 94 #[tokio::test]
+4 -3
crates/tranquil-pds/tests/auth_extractor.rs
··· 215 215 let did = account["did"].as_str().unwrap().to_string(); 216 216 verify_new_account(&http_client, &did).await; 217 217 218 - let pool = common::get_test_db_pool().await; 219 - sqlx::query!("UPDATE users SET is_admin = TRUE WHERE did = $1", &did) 220 - .execute(pool) 218 + let repos = common::get_test_repos().await; 219 + repos 220 + .user 221 + .set_admin_status(&tranquil_types::Did::new(did.clone()).unwrap(), true) 221 222 .await 222 223 .expect("Failed to mark user as admin"); 223 224
+105 -43
crates/tranquil-pds/tests/common/mod.rs
··· 28 28 static TEST_DB_POOL: OnceLock<sqlx::PgPool> = OnceLock::new(); 29 29 static TEST_TEMP_DIR: OnceLock<PathBuf> = OnceLock::new(); 30 30 static CLUSTER: OnceLock<Vec<ServerInstance>> = OnceLock::new(); 31 + static TEST_REPOS: OnceLock<Arc<tranquil_db::PostgresRepositories>> = OnceLock::new(); 32 + 33 + #[allow(dead_code)] 34 + pub fn is_store_backend() -> bool { 35 + std::env::var("TRANQUIL_TEST_BACKEND") 36 + .map(|v| v == "store") 37 + .unwrap_or(false) 38 + } 31 39 32 40 #[allow(dead_code)] 33 41 pub struct ServerConfig { 34 - pub pool: sqlx::PgPool, 42 + pub pool: Option<sqlx::PgPool>, 35 43 pub cache: Option<(Arc<dyn Cache>, Arc<dyn DistributedRateLimiter>)>, 36 44 } 37 45 ··· 123 131 SERVER_URL.get_or_init(|| { 124 132 let (tx, rx) = std::sync::mpsc::channel(); 125 133 std::thread::spawn(move || { 134 + let _ = tracing_subscriber::fmt() 135 + .with_env_filter( 136 + tracing_subscriber::EnvFilter::try_from_default_env() 137 + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")), 138 + ) 139 + .try_init(); 126 140 unsafe { 127 141 std::env::set_var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS", "1"); 128 142 } ··· 141 155 } 142 156 let rt = tokio::runtime::Runtime::new().unwrap(); 143 157 rt.block_on(async move { 144 - if has_external_infra() { 158 + if is_store_backend() { 159 + let url = setup_store_backend().await; 160 + tx.send(url).unwrap(); 161 + } else if has_external_infra() { 145 162 let url = setup_with_external_infra().await; 146 163 tx.send(url).unwrap(); 147 164 } else { ··· 557 574 .with_oauth_authorize_limit(10000) 558 575 .with_oauth_token_limit(10000); 559 576 let cache_refs = config.cache.as_ref().map(|(c, r)| (c.clone(), r.clone())); 560 - let mut state = AppState::from_db(config.pool, CancellationToken::new()) 561 - .await 562 - .with_rate_limiters(rate_limiters); 577 + let mut state = match config.pool { 578 + Some(pool) => AppState::from_db(pool, CancellationToken::new()).await, 579 + None => AppState::from_store(CancellationToken::new()).await, 580 + }; 581 + state = state.with_rate_limiters(rate_limiters); 582 + TEST_REPOS.set(state.repos.clone()).ok(); 563 583 if let Some((cache, distributed_rate_limiter)) = config.cache { 564 584 state = state.with_cache(cache, distributed_rate_limiter); 565 585 } ··· 590 610 } 591 611 } 592 612 613 + async fn setup_store_backend() -> String { 614 + let temp_dir = 615 + std::env::temp_dir().join(format!("tranquil-pds-store-{}", uuid::Uuid::new_v4())); 616 + let blob_path = temp_dir.join("blobs"); 617 + let backup_path = temp_dir.join("backups"); 618 + let store_path = temp_dir.join("store"); 619 + std::fs::create_dir_all(&blob_path).expect("failed to create blob temp directory"); 620 + std::fs::create_dir_all(&backup_path).expect("failed to create backup temp directory"); 621 + std::fs::create_dir_all(&store_path).expect("failed to create store temp directory"); 622 + TEST_TEMP_DIR.set(temp_dir).ok(); 623 + let plc_url = setup_mock_plc_directory().await; 624 + unsafe { 625 + std::env::set_var("BLOB_STORAGE_BACKEND", "filesystem"); 626 + std::env::set_var("BLOB_STORAGE_PATH", blob_path.to_str().unwrap()); 627 + std::env::set_var("BACKUP_STORAGE_BACKEND", "filesystem"); 628 + std::env::set_var("BACKUP_STORAGE_PATH", backup_path.to_str().unwrap()); 629 + std::env::set_var("MAX_IMPORT_SIZE", "100000000"); 630 + std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); 631 + std::env::set_var("PLC_DIRECTORY_URL", &plc_url); 632 + std::env::set_var("REPO_BACKEND", "tranquil-store"); 633 + std::env::set_var("TRANQUIL_STORE_DATA_DIR", store_path.to_str().unwrap()); 634 + std::env::set_var("DATABASE_URL", "postgres://unused/unused"); 635 + } 636 + register_mock_appview().await; 637 + let instance = spawn_server(ServerConfig { 638 + pool: None, 639 + cache: None, 640 + }) 641 + .await; 642 + APP_PORT.set(instance.port).ok(); 643 + instance.url 644 + } 645 + 593 646 async fn spawn_app(database_url: String) -> String { 594 647 let pool = PgPoolOptions::new() 595 648 .max_connections(10) ··· 608 661 .await 609 662 .expect("Failed to create test pool"); 610 663 TEST_DB_POOL.set(test_pool).ok(); 611 - let instance = spawn_server(ServerConfig { pool, cache: None }).await; 664 + let instance = spawn_server(ServerConfig { 665 + pool: Some(pool), 666 + cache: None, 667 + }) 668 + .await; 612 669 APP_PORT.set(instance.port).ok(); 613 670 instance.url 614 671 } ··· 659 716 let mut instances: Vec<ServerInstance> = Vec::with_capacity(node_count); 660 717 for (cache, rate_limiter) in ripple_nodes { 661 718 let server_config = ServerConfig { 662 - pool: pool.clone(), 719 + pool: Some(pool.clone()), 663 720 cache: Some((cache, rate_limiter)), 664 721 }; 665 722 let instance = spawn_server(server_config).await; ··· 799 856 } 800 857 801 858 #[allow(dead_code)] 802 - pub async fn verify_new_account(client: &Client, did: &str) -> String { 803 - let pool = get_test_db_pool().await; 804 - let body_text: String = sqlx::query_scalar!( 805 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 806 - did 807 - ) 808 - .fetch_one(pool) 809 - .await 810 - .expect("Failed to get verification code"); 859 + pub async fn get_test_repos() -> &'static Arc<tranquil_db::PostgresRepositories> { 860 + base_url().await; 861 + TEST_REPOS.get().expect("TEST_REPOS not initialized") 862 + } 811 863 864 + fn extract_verification_code(body_text: &str) -> String { 812 865 let lines: Vec<&str> = body_text.lines().collect(); 813 - let verification_code = lines 866 + lines 814 867 .iter() 815 868 .enumerate() 816 869 .find(|(_, line)| line.contains("verification code is:") || line.contains("code is:")) ··· 821 874 .find(|line| line.trim().starts_with("MX")) 822 875 .map(|s| s.trim().to_string()) 823 876 }) 824 - .unwrap_or_else(|| body_text.clone()); 877 + .unwrap_or_else(|| body_text.to_string()) 878 + } 879 + 880 + async fn get_verification_body_for_did(did: &str) -> String { 881 + use tranquil_db_traits::CommsType; 882 + use tranquil_types::Did; 883 + 884 + let repos = get_test_repos().await; 885 + let user = repos 886 + .user 887 + .get_by_did(&Did::new(did.to_string()).unwrap()) 888 + .await 889 + .expect("failed to look up user") 890 + .expect("user not found"); 891 + let comms = repos 892 + .infra 893 + .get_latest_comms_for_user(user.id, CommsType::EmailVerification, 1) 894 + .await 895 + .expect("failed to get comms"); 896 + comms 897 + .first() 898 + .map(|c| c.body.clone()) 899 + .expect("no email_verification comms found") 900 + } 901 + 902 + #[allow(dead_code)] 903 + pub async fn verify_new_account(client: &Client, did: &str) -> String { 904 + let body_text = get_verification_body_for_did(did).await; 905 + let verification_code = extract_verification_code(&body_text); 825 906 826 907 let confirm_payload = json!({ 827 908 "did": did, ··· 956 1037 if res.status() == StatusCode::OK { 957 1038 let body: Value = res.json().await.expect("Invalid JSON"); 958 1039 let did = body["did"].as_str().expect("No did").to_string(); 959 - let pool = get_test_db_pool().await; 960 1040 if make_admin { 961 - sqlx::query!("UPDATE users SET is_admin = TRUE WHERE did = $1", &did) 962 - .execute(pool) 1041 + let repos = get_test_repos().await; 1042 + repos 1043 + .user 1044 + .set_admin_status(&tranquil_types::Did::new(did.clone()).unwrap(), true) 963 1045 .await 964 1046 .expect("Failed to mark user as admin"); 965 1047 } ··· 969 1051 { 970 1052 return (access_jwt.to_string(), did); 971 1053 } 972 - let body_text: String = sqlx::query_scalar!( 973 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 974 - &did 975 - ) 976 - .fetch_one(pool) 977 - .await 978 - .expect("Failed to get verification from comms_queue"); 979 - let lines: Vec<&str> = body_text.lines().collect(); 980 - let verification_code = lines 981 - .iter() 982 - .enumerate() 983 - .find(|(_, line): &(usize, &&str)| { 984 - line.contains("verification code is:") || line.contains("code is:") 985 - }) 986 - .and_then(|(i, _)| lines.get(i + 1).map(|s: &&str| s.trim().to_string())) 987 - .or_else(|| { 988 - body_text 989 - .lines() 990 - .find(|line| line.trim().starts_with("MX")) 991 - .map(|s| s.trim().to_string()) 992 - }) 993 - .unwrap_or_else(|| body_text.clone()); 1054 + let body_text = get_verification_body_for_did(&did).await; 1055 + let verification_code = extract_verification_code(&body_text); 994 1056 995 1057 let confirm_payload = json!({ 996 1058 "did": did,
+51 -60
crates/tranquil-pds/tests/delete_account.rs
··· 51 51 .await 52 52 .expect("Failed to request account deletion"); 53 53 assert_eq!(request_delete_res.status(), StatusCode::OK); 54 - let pool = get_test_db_pool().await; 55 - let row = sqlx::query!( 56 - "SELECT token FROM account_deletion_requests WHERE did = $1", 57 - did 58 - ) 59 - .fetch_one(pool) 60 - .await 61 - .expect("Failed to query deletion token"); 62 - let token = row.token; 54 + let repos = get_test_repos().await; 55 + let deletion_request = repos 56 + .infra 57 + .get_deletion_request_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 58 + .await 59 + .unwrap() 60 + .unwrap(); 61 + let token = deletion_request.token; 63 62 let delete_payload = json!({ 64 63 "did": did, 65 64 "password": password, ··· 75 74 .await 76 75 .expect("Failed to delete account"); 77 76 assert_eq!(delete_res.status(), StatusCode::OK); 78 - let user_row = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 79 - .fetch_optional(pool) 77 + let user = repos 78 + .user 79 + .get_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 80 80 .await 81 - .expect("Failed to query user"); 82 - assert!(user_row.is_none(), "User should be deleted from database"); 81 + .unwrap(); 82 + assert!(user.is_none(), "User should be deleted from database"); 83 83 let session_res = client 84 84 .get(format!("{}/xrpc/com.atproto.server.getSession", base_url)) 85 85 .bearer_auth(&jwt) ··· 108 108 .await 109 109 .expect("Failed to request account deletion"); 110 110 assert_eq!(request_delete_res.status(), StatusCode::OK); 111 - let pool = get_test_db_pool().await; 112 - let row = sqlx::query!( 113 - "SELECT token FROM account_deletion_requests WHERE did = $1", 114 - did 115 - ) 116 - .fetch_one(pool) 117 - .await 118 - .expect("Failed to query deletion token"); 119 - let token = row.token; 111 + let repos = get_test_repos().await; 112 + let deletion_request = repos 113 + .infra 114 + .get_deletion_request_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 115 + .await 116 + .unwrap() 117 + .unwrap(); 118 + let token = deletion_request.token; 120 119 let delete_payload = json!({ 121 120 "did": did, 122 121 "password": "wrong-password", ··· 198 197 .await 199 198 .expect("Failed to request account deletion"); 200 199 assert_eq!(request_delete_res.status(), StatusCode::OK); 201 - let pool = get_test_db_pool().await; 202 - let row = sqlx::query!( 203 - "SELECT token FROM account_deletion_requests WHERE did = $1", 204 - did 205 - ) 206 - .fetch_one(pool) 207 - .await 208 - .expect("Failed to query deletion token"); 209 - let token = row.token; 210 - sqlx::query!( 211 - "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1", 212 - token 213 - ) 214 - .execute(pool) 215 - .await 216 - .expect("Failed to expire token"); 200 + let repos = get_test_repos().await; 201 + let deletion_request = repos 202 + .infra 203 + .get_deletion_request_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 204 + .await 205 + .unwrap() 206 + .unwrap(); 207 + let token = deletion_request.token; 208 + repos.infra.expire_deletion_request(&token).await.unwrap(); 217 209 let delete_payload = json!({ 218 210 "did": did, 219 211 "password": password, ··· 257 249 .await 258 250 .expect("Failed to request account deletion"); 259 251 assert_eq!(request_delete_res.status(), StatusCode::OK); 260 - let pool = get_test_db_pool().await; 261 - let row = sqlx::query!( 262 - "SELECT token FROM account_deletion_requests WHERE did = $1", 263 - did1 264 - ) 265 - .fetch_one(pool) 266 - .await 267 - .expect("Failed to query deletion token"); 268 - let token = row.token; 252 + let repos = get_test_repos().await; 253 + let deletion_request = repos 254 + .infra 255 + .get_deletion_request_by_did(&tranquil_types::Did::new(did1.clone()).unwrap()) 256 + .await 257 + .unwrap() 258 + .unwrap(); 259 + let token = deletion_request.token; 269 260 let delete_payload = json!({ 270 261 "did": did2, 271 262 "password": password2, ··· 318 309 .await 319 310 .expect("Failed to request account deletion"); 320 311 assert_eq!(request_delete_res.status(), StatusCode::OK); 321 - let pool = get_test_db_pool().await; 322 - let row = sqlx::query!( 323 - "SELECT token FROM account_deletion_requests WHERE did = $1", 324 - did 325 - ) 326 - .fetch_one(pool) 327 - .await 328 - .expect("Failed to query deletion token"); 329 - let token = row.token; 312 + let repos = get_test_repos().await; 313 + let deletion_request = repos 314 + .infra 315 + .get_deletion_request_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 316 + .await 317 + .unwrap() 318 + .unwrap(); 319 + let token = deletion_request.token; 330 320 let delete_payload = json!({ 331 321 "did": did, 332 322 "password": app_password, ··· 342 332 .await 343 333 .expect("Failed to delete account"); 344 334 assert_eq!(delete_res.status(), StatusCode::OK); 345 - let user_row = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 346 - .fetch_optional(pool) 335 + let user = repos 336 + .user 337 + .get_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 347 338 .await 348 - .expect("Failed to query user"); 349 - assert!(user_row.is_none(), "User should be deleted from database"); 339 + .unwrap(); 340 + assert!(user.is_none(), "User should be deleted from database"); 350 341 } 351 342 352 343 #[tokio::test]
+89 -50
crates/tranquil-pds/tests/email_update.rs
··· 1 1 mod common; 2 2 use reqwest::StatusCode; 3 3 use serde_json::{Value, json}; 4 - use sqlx::PgPool; 4 + use tranquil_db_traits::CommsType; 5 + use tranquil_types::Did; 5 6 6 - async fn get_email_update_token(pool: &PgPool, did: &str) -> String { 7 - let body_text: String = sqlx::query_scalar!( 8 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1", 9 - did 10 - ) 11 - .fetch_one(pool) 12 - .await 13 - .expect("Verification not found"); 7 + async fn get_email_update_token(did: &str) -> String { 8 + let repos = common::get_test_repos().await; 9 + let parsed_did = Did::new(did.to_string()).unwrap(); 10 + let user = repos 11 + .user 12 + .get_by_did(&parsed_did) 13 + .await 14 + .expect("failed to look up user") 15 + .expect("user not found"); 16 + let comms = repos 17 + .infra 18 + .get_latest_comms_for_user(user.id, CommsType::EmailUpdate, 1) 19 + .await 20 + .expect("failed to get comms"); 21 + let body_text = comms.first().expect("Verification not found").body.clone(); 14 22 15 23 body_text 16 24 .lines() ··· 82 90 async fn test_update_email_flow_success() { 83 91 let client = common::client(); 84 92 let base_url = common::base_url().await; 85 - let pool = common::get_test_db_pool().await; 93 + let repos = common::get_test_repos().await; 86 94 let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 87 95 let email = format!("{}@example.com", handle); 88 96 let (access_jwt, did) = create_verified_account(&client, base_url, &handle, &email).await; ··· 101 109 let body: Value = res.json().await.expect("Invalid JSON"); 102 110 assert_eq!(body["tokenRequired"], true); 103 111 104 - let code = get_email_update_token(pool, &did).await; 112 + let code = get_email_update_token(&did).await; 105 113 106 114 let res = client 107 115 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) ··· 115 123 .expect("Failed to update email"); 116 124 assert_eq!(res.status(), StatusCode::OK); 117 125 118 - let user_email: Option<String> = 119 - sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did) 120 - .fetch_one(pool) 121 - .await 122 - .expect("User not found"); 126 + let parsed_did = Did::new(did).unwrap(); 127 + let user_email = repos 128 + .user 129 + .get_email_info_by_did(&parsed_did) 130 + .await 131 + .expect("failed to look up user") 132 + .expect("user not found") 133 + .email; 123 134 assert_eq!(user_email, Some(new_email)); 124 135 } 125 136 ··· 239 250 async fn test_confirm_email_confirms_existing_email() { 240 251 let client = common::client(); 241 252 let base_url = common::base_url().await; 242 - let pool = common::get_test_db_pool().await; 253 + let repos = common::get_test_repos().await; 243 254 let handle = format!("ec{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 244 255 let email = format!("{}@example.com", handle); 245 256 ··· 264 275 .expect("No accessJwt") 265 276 .to_string(); 266 277 267 - let body_text: String = sqlx::query_scalar!( 268 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 269 - did 270 - ) 271 - .fetch_one(pool) 272 - .await 273 - .expect("Verification email not found"); 278 + let parsed_did = Did::new(did.clone()).unwrap(); 279 + let user = repos 280 + .user 281 + .get_by_did(&parsed_did) 282 + .await 283 + .expect("failed to look up user") 284 + .expect("user not found"); 285 + let comms = repos 286 + .infra 287 + .get_latest_comms_for_user(user.id, CommsType::EmailVerification, 1) 288 + .await 289 + .expect("failed to get comms"); 290 + let body_text = comms 291 + .first() 292 + .expect("Verification email not found") 293 + .body 294 + .clone(); 274 295 275 296 let code = body_text 276 297 .lines() ··· 290 311 .expect("Failed to confirm email"); 291 312 assert_eq!(res.status(), StatusCode::OK); 292 313 293 - let verified: bool = 294 - sqlx::query_scalar!("SELECT email_verified FROM users WHERE did = $1", did) 295 - .fetch_one(pool) 296 - .await 297 - .expect("User not found"); 314 + let verified = repos 315 + .user 316 + .get_email_info_by_did(&parsed_did) 317 + .await 318 + .expect("failed to look up user") 319 + .expect("user not found") 320 + .email_verified; 298 321 assert!(verified); 299 322 } 300 323 ··· 302 325 async fn test_confirm_email_rejects_wrong_email() { 303 326 let client = common::client(); 304 327 let base_url = common::base_url().await; 305 - let pool = common::get_test_db_pool().await; 328 + let repos = common::get_test_repos().await; 306 329 let handle = format!("ew{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 307 330 let email = format!("{}@example.com", handle); 308 331 ··· 327 350 .expect("No accessJwt") 328 351 .to_string(); 329 352 330 - let body_text: String = sqlx::query_scalar!( 331 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 332 - did 333 - ) 334 - .fetch_one(pool) 335 - .await 336 - .expect("Verification email not found"); 353 + let parsed_did = Did::new(did).unwrap(); 354 + let user = repos 355 + .user 356 + .get_by_did(&parsed_did) 357 + .await 358 + .expect("failed to look up user") 359 + .expect("user not found"); 360 + let comms = repos 361 + .infra 362 + .get_latest_comms_for_user(user.id, CommsType::EmailVerification, 1) 363 + .await 364 + .expect("failed to get comms"); 365 + let body_text = comms 366 + .first() 367 + .expect("Verification email not found") 368 + .body 369 + .clone(); 337 370 338 371 let code = body_text 339 372 .lines() ··· 402 435 async fn test_unverified_account_can_update_email_without_token() { 403 436 let client = common::client(); 404 437 let base_url = common::base_url().await; 405 - let pool = common::get_test_db_pool().await; 438 + let repos = common::get_test_repos().await; 406 439 let handle = format!("ev{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 407 440 let email = format!("{}@example.com", handle); 408 441 ··· 457 490 "Unverified account should be able to update email without token" 458 491 ); 459 492 460 - let user_email: Option<String> = 461 - sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did) 462 - .fetch_one(pool) 463 - .await 464 - .expect("User not found"); 493 + let parsed_did = Did::new(did).unwrap(); 494 + let user_email = repos 495 + .user 496 + .get_email_info_by_did(&parsed_did) 497 + .await 498 + .expect("failed to look up user") 499 + .expect("user not found") 500 + .email; 465 501 assert_eq!(user_email, Some(new_email)); 466 502 } 467 503 ··· 469 505 async fn test_update_email_to_same_as_another_user_allowed() { 470 506 let client = common::client(); 471 507 let base_url = common::base_url().await; 472 - let pool = common::get_test_db_pool().await; 508 + let repos = common::get_test_repos().await; 473 509 474 510 let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 475 511 let email1 = format!("{}@example.com", handle1); ··· 490 526 .expect("Failed to request email update"); 491 527 assert_eq!(res.status(), StatusCode::OK); 492 528 493 - let code = get_email_update_token(pool, &did2).await; 529 + let code = get_email_update_token(&did2).await; 494 530 495 531 let res = client 496 532 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) ··· 508 544 "Multiple accounts can share the same email address" 509 545 ); 510 546 511 - let user_email: Option<String> = 512 - sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did2) 513 - .fetch_one(pool) 514 - .await 515 - .expect("User not found"); 547 + let parsed_did = Did::new(did2).unwrap(); 548 + let user_email = repos 549 + .user 550 + .get_email_info_by_did(&parsed_did) 551 + .await 552 + .expect("failed to look up user") 553 + .expect("user not found") 554 + .email; 516 555 assert_eq!(user_email, Some(email1.clone())); 517 556 }
+2 -5
crates/tranquil-pds/tests/firehose_validation.rs
··· 800 800 801 801 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 802 802 803 - let pool = get_test_db_pool().await; 804 - let max_seq: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 805 - .fetch_one(pool) 806 - .await 807 - .unwrap(); 803 + let repos = get_test_repos().await; 804 + let max_seq = repos.repo.get_max_seq().await.unwrap().as_i64(); 808 805 let outdated_cursor = (max_seq - 100).max(1); 809 806 let url = format!( 810 807 "ws://127.0.0.1:{}/xrpc/com.atproto.sync.subscribeRepos?cursor={}",
+7 -15
crates/tranquil-pds/tests/helpers/mod.rs
··· 482 482 483 483 #[allow(dead_code)] 484 484 pub async fn get_user_signing_key(did: &str) -> Option<Vec<u8>> { 485 - let db_url = get_db_connection_string().await; 486 - let pool = sqlx::PgPool::connect(&db_url).await.ok()?; 487 - let row = sqlx::query!( 488 - r#" 489 - SELECT k.key_bytes, k.encryption_version 490 - FROM user_keys k 491 - JOIN users u ON k.user_id = u.id 492 - WHERE u.did = $1 493 - "#, 494 - did 495 - ) 496 - .fetch_optional(&pool) 497 - .await 498 - .ok()??; 499 - tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() 485 + let repos = super::common::get_test_repos().await; 486 + let key_info = repos 487 + .user 488 + .get_user_key_by_did(&tranquil_types::Did::new(did.to_string()).ok()?) 489 + .await 490 + .ok()??; 491 + tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version).ok() 500 492 }
+1
crates/tranquil-pds/tests/invite.rs
··· 203 203 #[tokio::test] 204 204 async fn test_create_invite_codes_non_admin() { 205 205 let client = client(); 206 + let _ = create_account_and_login(&client).await; 206 207 let (access_jwt, _did) = create_account_and_login(&client).await; 207 208 let payload = json!({ 208 209 "useCount": 2
+14 -6
crates/tranquil-pds/tests/jwt_security.rs
··· 2 2 mod common; 3 3 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4 4 use chrono::{Duration, Utc}; 5 - use common::{base_url, client, create_account_and_login, get_test_db_pool}; 5 + use common::{base_url, client, create_account_and_login, get_test_repos}; 6 6 use k256::SecretKey; 7 7 use k256::ecdsa::{Signature, SigningKey, signature::Signer}; 8 8 use rand::rngs::OsRng; ··· 691 691 let account: Value = create_res.json().await.unwrap(); 692 692 let did = account["did"].as_str().unwrap(); 693 693 694 - let pool = get_test_db_pool().await; 695 - let body_text: String = sqlx::query_scalar!( 696 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 697 - did 698 - ).fetch_one(pool).await.unwrap(); 694 + let repos = get_test_repos().await; 695 + let user = repos 696 + .user 697 + .get_by_did(&tranquil_types::Did::new(did.to_string()).unwrap()) 698 + .await 699 + .unwrap() 700 + .unwrap(); 701 + let comms = repos 702 + .infra 703 + .get_latest_comms_for_user(user.id, tranquil_db_traits::CommsType::EmailVerification, 1) 704 + .await 705 + .unwrap(); 706 + let body_text = comms.first().unwrap().body.clone(); 699 707 let lines: Vec<&str> = body_text.lines().collect(); 700 708 let code = lines 701 709 .iter()
+79 -112
crates/tranquil-pds/tests/legacy_2fa.rs
··· 1 1 mod common; 2 2 3 - use common::{base_url, client, create_account_and_login, get_test_db_pool}; 3 + use common::{base_url, client, create_account_and_login, get_test_repos}; 4 4 use reqwest::StatusCode; 5 5 use serde_json::{Value, json}; 6 + use tranquil_db_traits::CommsType; 7 + use tranquil_types::Did; 6 8 7 9 async fn enable_totp_for_user(did: &str) { 8 - let pool = get_test_db_pool().await; 9 - let secret = vec![0u8; 20]; 10 - sqlx::query( 11 - r#"INSERT INTO user_totp (did, secret_encrypted, encryption_version, verified, created_at) 12 - VALUES ($1, $2, 1, TRUE, NOW()) 13 - ON CONFLICT (did) DO UPDATE SET verified = TRUE"#, 14 - ) 15 - .bind(did) 16 - .bind(&secret) 17 - .execute(pool) 18 - .await 19 - .expect("Failed to enable TOTP"); 10 + let repos = get_test_repos().await; 11 + repos 12 + .user 13 + .enable_totp_verified(&Did::new(did.to_string()).unwrap(), &[0u8; 20]) 14 + .await 15 + .unwrap(); 20 16 } 21 17 22 18 async fn set_allow_legacy_login(did: &str, allow: bool) { 23 - let pool = get_test_db_pool().await; 24 - sqlx::query("UPDATE users SET allow_legacy_login = $1 WHERE did = $2") 25 - .bind(allow) 26 - .bind(did) 27 - .execute(pool) 19 + let repos = get_test_repos().await; 20 + repos 21 + .user 22 + .update_legacy_login(&Did::new(did.to_string()).unwrap(), allow) 28 23 .await 29 - .expect("Failed to set allow_legacy_login"); 24 + .unwrap(); 30 25 } 31 26 32 27 async fn get_2fa_code_from_queue(did: &str) -> Option<String> { 33 - let pool = get_test_db_pool().await; 34 - let row: Option<(String,)> = sqlx::query_as( 35 - r#"SELECT body FROM comms_queue 36 - WHERE user_id = (SELECT id FROM users WHERE did = $1) 37 - AND comms_type = 'two_factor_code' 38 - ORDER BY created_at DESC LIMIT 1"#, 39 - ) 40 - .bind(did) 41 - .fetch_optional(pool) 42 - .await 43 - .ok() 44 - .flatten(); 28 + let repos = get_test_repos().await; 29 + let parsed_did = Did::new(did.to_string()).unwrap(); 30 + let user_id = repos 31 + .user 32 + .get_id_by_did(&parsed_did) 33 + .await 34 + .expect("DB error") 35 + .expect("User not found"); 45 36 46 - row.and_then(|(body,)| { 47 - body.lines() 37 + let comms = repos 38 + .infra 39 + .get_latest_comms_for_user(user_id, CommsType::TwoFactorCode, 1) 40 + .await 41 + .ok()?; 42 + 43 + comms.first().and_then(|c| { 44 + c.body 45 + .lines() 48 46 .find(|line: &&str| line.chars().all(|c: char| c.is_ascii_digit()) && line.len() == 8) 49 47 .map(|s: &str| s.to_string()) 50 48 .or_else(|| { 51 - body.split_whitespace() 49 + c.body 50 + .split_whitespace() 52 51 .find(|word: &&str| { 53 52 word.chars().all(|c: char| c.is_ascii_digit()) && word.len() == 8 54 53 }) ··· 58 57 } 59 58 60 59 async fn clear_2fa_challenges_for_user(did: &str) { 61 - let pool = get_test_db_pool().await; 62 - let _ = sqlx::query( 63 - "DELETE FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'two_factor_code'", 64 - ) 65 - .bind(did) 66 - .execute(pool) 67 - .await; 60 + let repos = get_test_repos().await; 61 + let parsed_did = Did::new(did.to_string()).unwrap(); 62 + let user_id = repos 63 + .user 64 + .get_id_by_did(&parsed_did) 65 + .await 66 + .expect("DB error") 67 + .expect("User not found"); 68 + 69 + let _ = repos 70 + .infra 71 + .delete_comms_by_type_for_user(user_id, CommsType::TwoFactorCode) 72 + .await; 68 73 } 69 74 70 75 async fn set_email_auth_factor(did: &str, enabled: bool) { 71 - let pool = get_test_db_pool().await; 72 - let user_id: uuid::Uuid = 73 - sqlx::query_scalar::<_, uuid::Uuid>("SELECT id FROM users WHERE did = $1") 74 - .bind(did) 75 - .fetch_one(pool) 76 - .await 77 - .expect("Failed to get user id"); 78 - let pool = get_test_db_pool().await; 79 - let _ = sqlx::query( 80 - "DELETE FROM account_preferences WHERE user_id = $1 AND name = 'email_auth_factor'", 81 - ) 82 - .bind(user_id) 83 - .execute(pool) 84 - .await; 85 - let pool = get_test_db_pool().await; 86 - sqlx::query( 87 - "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, 'email_auth_factor', $2::jsonb)", 88 - ) 89 - .bind(user_id) 90 - .bind(serde_json::json!(enabled)) 91 - .execute(pool) 92 - .await 93 - .expect("Failed to set email_auth_factor"); 76 + let repos = get_test_repos().await; 77 + let parsed_did = Did::new(did.to_string()).unwrap(); 78 + let user_id = repos 79 + .user 80 + .get_id_by_did(&parsed_did) 81 + .await 82 + .expect("DB error") 83 + .expect("User not found"); 84 + 85 + repos 86 + .infra 87 + .upsert_account_preference(user_id, "email_auth_factor", serde_json::json!(enabled)) 88 + .await 89 + .expect("Failed to set email_auth_factor"); 90 + } 91 + 92 + async fn get_handle(did: &str) -> String { 93 + let repos = get_test_repos().await; 94 + repos 95 + .user 96 + .get_handle_by_did(&Did::new(did.to_string()).unwrap()) 97 + .await 98 + .expect("DB error") 99 + .expect("Handle not found") 100 + .to_string() 94 101 } 95 102 96 103 #[tokio::test] ··· 102 109 enable_totp_for_user(&did).await; 103 110 set_allow_legacy_login(&did, true).await; 104 111 105 - let pool = get_test_db_pool().await; 106 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 107 - .bind(&did) 108 - .fetch_one(pool) 109 - .await 110 - .expect("Failed to get handle"); 112 + let handle = get_handle(&did).await; 111 113 112 114 let login_payload = json!({ 113 115 "identifier": handle, ··· 141 143 set_allow_legacy_login(&did, true).await; 142 144 clear_2fa_challenges_for_user(&did).await; 143 145 144 - let pool = get_test_db_pool().await; 145 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 146 - .bind(&did) 147 - .fetch_one(pool) 148 - .await 149 - .expect("Failed to get handle"); 146 + let handle = get_handle(&did).await; 150 147 151 148 let login_payload = json!({ 152 149 "identifier": handle, ··· 194 191 set_allow_legacy_login(&did, true).await; 195 192 clear_2fa_challenges_for_user(&did).await; 196 193 197 - let pool = get_test_db_pool().await; 198 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 199 - .bind(&did) 200 - .fetch_one(pool) 201 - .await 202 - .expect("Failed to get handle"); 194 + let handle = get_handle(&did).await; 203 195 204 196 let resp = client 205 197 .post(format!("{}/xrpc/com.atproto.server.createSession", base)) ··· 245 237 enable_totp_for_user(&did).await; 246 238 set_allow_legacy_login(&did, false).await; 247 239 248 - let pool = get_test_db_pool().await; 249 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 250 - .bind(&did) 251 - .fetch_one(pool) 252 - .await 253 - .expect("Failed to get handle"); 240 + let handle = get_handle(&did).await; 254 241 255 242 let login_payload = json!({ 256 243 "identifier": handle, ··· 274 261 let base = base_url().await; 275 262 let (_token, did) = create_account_and_login(&client).await; 276 263 277 - let pool = get_test_db_pool().await; 278 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 279 - .bind(&did) 280 - .fetch_one(pool) 281 - .await 282 - .expect("Failed to get handle"); 264 + let handle = get_handle(&did).await; 283 265 284 266 let login_payload = json!({ 285 267 "identifier": handle, ··· 307 289 set_allow_legacy_login(&did, true).await; 308 290 clear_2fa_challenges_for_user(&did).await; 309 291 310 - let pool = get_test_db_pool().await; 311 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 312 - .bind(&did) 313 - .fetch_one(pool) 314 - .await 315 - .expect("Failed to get handle"); 292 + let handle = get_handle(&did).await; 316 293 317 294 let resp = client 318 295 .post(format!("{}/xrpc/com.atproto.server.createSession", base)) ··· 404 381 set_email_auth_factor(&did, true).await; 405 382 clear_2fa_challenges_for_user(&did).await; 406 383 407 - let pool = get_test_db_pool().await; 408 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 409 - .bind(&did) 410 - .fetch_one(pool) 411 - .await 412 - .expect("Failed to get handle"); 384 + let handle = get_handle(&did).await; 413 385 414 386 let login_payload = json!({ 415 387 "identifier": handle, ··· 457 429 458 430 set_email_auth_factor(&did, false).await; 459 431 460 - let pool = get_test_db_pool().await; 461 - let handle: String = sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1") 462 - .bind(&did) 463 - .fetch_one(pool) 464 - .await 465 - .expect("Failed to get handle"); 432 + let handle = get_handle(&did).await; 466 433 467 434 let login_payload = json!({ 468 435 "identifier": handle,
+18 -14
crates/tranquil-pds/tests/lifecycle_session.rs
··· 577 577 .await 578 578 .expect("Failed to request account deletion"); 579 579 assert_eq!(res.status(), StatusCode::OK); 580 - let db_url = get_db_connection_string().await; 581 - let pool = sqlx::PgPool::connect(&db_url) 580 + let repos = get_test_repos().await; 581 + let deletion_request = repos 582 + .infra 583 + .get_deletion_request_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 582 584 .await 583 - .expect("Failed to connect to test DB"); 584 - let row = sqlx::query!( 585 - "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1", 586 - did 587 - ) 588 - .fetch_optional(&pool) 589 - .await 590 - .expect("Failed to query DB"); 591 - assert!(row.is_some(), "Deletion token should exist in DB"); 592 - let row = row.unwrap(); 593 - assert!(!row.token.is_empty(), "Token should not be empty"); 594 - assert!(row.expires_at > Utc::now(), "Token should not be expired"); 585 + .expect("Failed to query DB"); 586 + assert!( 587 + deletion_request.is_some(), 588 + "Deletion token should exist in DB" 589 + ); 590 + let deletion_request = deletion_request.unwrap(); 591 + assert!( 592 + !deletion_request.token.is_empty(), 593 + "Token should not be empty" 594 + ); 595 + assert!( 596 + deletion_request.expires_at > Utc::now(), 597 + "Token should not be expired" 598 + ); 595 599 }
+63 -74
crates/tranquil-pds/tests/notifications.rs
··· 1 1 mod common; 2 - use sqlx::Row; 3 - use tranquil_pds::comms::{CommsChannel, CommsStatus, CommsType}; 2 + use tranquil_db_traits::{CommsChannel, CommsStatus, CommsType}; 3 + use tranquil_types::Did; 4 4 5 5 #[tokio::test] 6 6 async fn test_enqueue_comms() { 7 - let pool = common::get_test_db_pool().await; 7 + let repos = common::get_test_repos().await; 8 8 let (_, did) = common::create_account_and_login(&common::client()).await; 9 - let user_id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE did = $1") 10 - .bind(&did) 11 - .fetch_one(pool) 9 + let user_id = repos 10 + .user 11 + .get_id_by_did(&Did::new(did).unwrap()) 12 12 .await 13 + .expect("DB error") 13 14 .expect("User not found"); 14 - let comms_id: uuid::Uuid = sqlx::query_scalar( 15 - r#"INSERT INTO comms_queue (user_id, channel, comms_type, recipient, subject, body) 16 - VALUES ($1, 'email', 'welcome', $2, $3, $4) 17 - RETURNING id"#, 18 - ) 19 - .bind(user_id) 20 - .bind("test@example.com") 21 - .bind("Test Subject") 22 - .bind("Test body") 23 - .fetch_one(pool) 24 - .await 25 - .expect("Failed to enqueue comms"); 26 - let row = sqlx::query( 27 - r#" 28 - SELECT id, user_id, recipient, subject, body, channel, comms_type, status 29 - FROM comms_queue 30 - WHERE id = $1 31 - "#, 32 - ) 33 - .bind(comms_id) 34 - .fetch_one(pool) 35 - .await 36 - .expect("Comms not found"); 37 - let row_user_id: uuid::Uuid = row.get("user_id"); 38 - let row_recipient: String = row.get("recipient"); 39 - let row_subject: Option<String> = row.get("subject"); 40 - let row_body: String = row.get("body"); 41 - let row_channel: CommsChannel = row.get("channel"); 42 - let row_comms_type: CommsType = row.get("comms_type"); 43 - let row_status: CommsStatus = row.get("status"); 44 - assert_eq!(row_user_id, user_id); 45 - assert_eq!(row_recipient, "test@example.com"); 46 - assert_eq!(row_subject.as_deref(), Some("Test Subject")); 47 - assert_eq!(row_body, "Test body"); 48 - assert_eq!(row_channel, CommsChannel::Email); 49 - assert_eq!(row_comms_type, CommsType::Welcome); 50 - assert_eq!(row_status, CommsStatus::Pending); 15 + repos 16 + .infra 17 + .enqueue_comms( 18 + Some(user_id), 19 + CommsChannel::Email, 20 + CommsType::Welcome, 21 + "test@example.com", 22 + Some("Test Subject"), 23 + "Test body", 24 + None, 25 + ) 26 + .await 27 + .expect("Failed to enqueue comms"); 28 + let comms = repos 29 + .infra 30 + .get_latest_comms_for_user(user_id, CommsType::Welcome, 1) 31 + .await 32 + .expect("DB error"); 33 + let row = comms.first().expect("Comms not found"); 34 + assert_eq!(row.user_id, Some(user_id)); 35 + assert_eq!(row.recipient, "test@example.com"); 36 + assert_eq!(row.subject.as_deref(), Some("Test Subject")); 37 + assert_eq!(row.body, "Test body"); 38 + assert_eq!(row.channel, CommsChannel::Email); 39 + assert_eq!(row.comms_type, CommsType::Welcome); 40 + assert_eq!(row.status, CommsStatus::Pending); 51 41 } 52 42 53 43 #[tokio::test] 54 44 async fn test_comms_queue_status_index() { 55 - let pool = common::get_test_db_pool().await; 45 + let repos = common::get_test_repos().await; 56 46 let (_, did) = common::create_account_and_login(&common::client()).await; 57 - let user_id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE did = $1") 58 - .bind(&did) 59 - .fetch_one(pool) 47 + let user_id = repos 48 + .user 49 + .get_id_by_did(&Did::new(did).unwrap()) 60 50 .await 51 + .expect("DB error") 61 52 .expect("User not found"); 62 - let initial_count: i64 = sqlx::query_scalar( 63 - "SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1", 64 - ) 65 - .bind(user_id) 66 - .fetch_one(pool) 67 - .await 68 - .expect("Failed to count"); 69 - let inserts = (0..5).map(|i| { 70 - sqlx::query( 71 - r#"INSERT INTO comms_queue (user_id, channel, comms_type, recipient, subject, body) 72 - VALUES ($1, 'email', 'password_reset', $2, $3, $4)"#, 73 - ) 74 - .bind(user_id) 75 - .bind(format!("test{}@example.com", i)) 76 - .bind("Test") 77 - .bind("Body") 78 - .execute(pool) 79 - }); 80 - futures::future::try_join_all(inserts) 53 + let initial_count = repos 54 + .infra 55 + .count_comms_by_type(user_id, CommsType::PasswordReset) 81 56 .await 82 - .expect("Failed to enqueue"); 83 - let final_count: i64 = sqlx::query_scalar( 84 - "SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1", 85 - ) 86 - .bind(user_id) 87 - .fetch_one(pool) 88 - .await 89 - .expect("Failed to count"); 57 + .expect("Failed to count"); 58 + for i in 0..5 { 59 + let recipient = format!("test{}@example.com", i); 60 + repos 61 + .infra 62 + .enqueue_comms( 63 + Some(user_id), 64 + CommsChannel::Email, 65 + CommsType::PasswordReset, 66 + &recipient, 67 + Some("Test"), 68 + "Body", 69 + None, 70 + ) 71 + .await 72 + .expect("Failed to enqueue"); 73 + } 74 + let final_count = repos 75 + .infra 76 + .count_comms_by_type(user_id, CommsType::PasswordReset) 77 + .await 78 + .expect("Failed to count"); 90 79 assert_eq!(final_count - initial_count, 5); 91 80 }
+26 -25
crates/tranquil-pds/tests/oauth.rs
··· 1 1 mod common; 2 2 mod helpers; 3 3 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4 - use common::{base_url, client, get_test_db_pool}; 4 + use common::{base_url, client, get_test_repos}; 5 5 use helpers::verify_new_account; 6 6 use reqwest::{StatusCode, redirect}; 7 7 use serde_json::{Value, json}; 8 8 use sha2::{Digest, Sha256}; 9 + use tranquil_types::{Did, RequestId}; 9 10 use wiremock::matchers::{method, path}; 10 11 use wiremock::{Mock, MockServer, ResponseTemplate}; 11 12 ··· 449 450 let account: Value = create_res.json().await.unwrap(); 450 451 let user_did = account["did"].as_str().unwrap(); 451 452 verify_new_account(&http_client, user_did).await; 452 - let pool = get_test_db_pool().await; 453 - sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 454 - .bind(user_did) 455 - .execute(pool) 453 + let repos = get_test_repos().await; 454 + repos 455 + .user 456 + .set_two_factor_enabled(&Did::new(user_did.to_string()).unwrap(), true) 456 457 .await 457 458 .unwrap(); 458 459 let redirect_uri = "https://example.com/2fa-callback"; ··· 508 509 .contains("Invalid") 509 510 || body["error"].as_str().unwrap_or("") == "invalid_code" 510 511 ); 511 - let twofa_code: String = 512 - sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 513 - .bind(request_uri) 514 - .fetch_one(pool) 515 - .await 516 - .unwrap(); 512 + let twofa_code: String = repos 513 + .oauth 514 + .get_2fa_challenge_code(&RequestId::new(request_uri.to_string())) 515 + .await 516 + .unwrap() 517 + .unwrap(); 517 518 let twofa_res = http_client 518 519 .post(format!("{}/oauth/authorize/2fa", url)) 519 520 .header("Content-Type", "application/json") ··· 574 575 let account: Value = create_res.json().await.unwrap(); 575 576 let user_did = account["did"].as_str().unwrap(); 576 577 verify_new_account(&http_client, user_did).await; 577 - let pool = get_test_db_pool().await; 578 - sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 579 - .bind(user_did) 580 - .execute(pool) 578 + let repos = get_test_repos().await; 579 + repos 580 + .user 581 + .set_two_factor_enabled(&Did::new(user_did.to_string()).unwrap(), true) 581 582 .await 582 583 .unwrap(); 583 584 let redirect_uri = "https://example.com/2fa-lockout-callback"; ··· 748 749 .json::<Value>() 749 750 .await 750 751 .unwrap(); 751 - let pool = get_test_db_pool().await; 752 - sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 753 - .bind(&user_did) 754 - .execute(pool) 752 + let repos = get_test_repos().await; 753 + repos 754 + .user 755 + .set_two_factor_enabled(&Did::new(user_did.to_string()).unwrap(), true) 755 756 .await 756 757 .unwrap(); 757 758 let (code_verifier2, code_challenge2) = generate_pkce(); ··· 789 790 select_body["needs_2fa"].as_bool().unwrap_or(false), 790 791 "Should need 2FA" 791 792 ); 792 - let twofa_code: String = 793 - sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 794 - .bind(request_uri2) 795 - .fetch_one(pool) 796 - .await 797 - .unwrap(); 793 + let twofa_code: String = repos 794 + .oauth 795 + .get_2fa_challenge_code(&RequestId::new(request_uri2.to_string())) 796 + .await 797 + .unwrap() 798 + .unwrap(); 798 799 let twofa_res = http_client 799 800 .post(format!("{}/oauth/authorize/2fa", url)) 800 801 .header("cookie", &device_cookie)
+64 -74
crates/tranquil-pds/tests/password_reset.rs
··· 3 3 use helpers::verify_new_account; 4 4 use reqwest::StatusCode; 5 5 use serde_json::{Value, json}; 6 + use tranquil_db_traits::CommsType; 6 7 7 8 #[tokio::test] 8 9 async fn test_request_password_reset_creates_code() { 9 10 let client = common::client(); 10 11 let base_url = common::base_url().await; 11 - let pool = common::get_test_db_pool().await; 12 + let repos = common::get_test_repos().await; 12 13 let handle = format!("pr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 13 14 let email = format!("{}@example.com", handle); 14 15 let payload = json!({ ··· 36 37 .await 37 38 .expect("Failed to request password reset"); 38 39 assert_eq!(res.status(), StatusCode::OK); 39 - let user = sqlx::query!( 40 - "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1", 41 - email 42 - ) 43 - .fetch_one(pool) 44 - .await 45 - .expect("User not found"); 46 - assert!(user.password_reset_code.is_some()); 47 - assert!(user.password_reset_code_expires_at.is_some()); 48 - let code = user.password_reset_code.unwrap(); 40 + let info = repos 41 + .user 42 + .get_password_reset_info(&email) 43 + .await 44 + .expect("failed to look up user") 45 + .expect("user not found"); 46 + assert!(info.code.is_some()); 47 + assert!(info.expires_at.is_some()); 48 + let code = info.code.unwrap(); 49 49 assert!(code.contains('-')); 50 50 assert_eq!(code.len(), 11); 51 51 } ··· 70 70 async fn test_reset_password_with_valid_token() { 71 71 let client = common::client(); 72 72 let base_url = common::base_url().await; 73 - let pool = common::get_test_db_pool().await; 73 + let repos = common::get_test_repos().await; 74 74 let handle = format!("pr2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 75 75 let email = format!("{}@example.com", handle); 76 76 let old_password = "Oldpass123!"; ··· 103 103 .await 104 104 .expect("Failed to request password reset"); 105 105 assert_eq!(res.status(), StatusCode::OK); 106 - let user = sqlx::query!( 107 - "SELECT password_reset_code FROM users WHERE email = $1", 108 - email 109 - ) 110 - .fetch_one(pool) 111 - .await 112 - .expect("User not found"); 113 - let token = user.password_reset_code.expect("No reset code"); 106 + let info = repos 107 + .user 108 + .get_password_reset_info(&email) 109 + .await 110 + .expect("failed to look up user") 111 + .expect("user not found"); 112 + let token = info.code.expect("No reset code"); 114 113 let res = client 115 114 .post(format!( 116 115 "{}/xrpc/com.atproto.server.resetPassword", ··· 124 123 .await 125 124 .expect("Failed to reset password"); 126 125 assert_eq!(res.status(), StatusCode::OK); 127 - let user = sqlx::query!( 128 - "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1", 129 - email 130 - ) 131 - .fetch_one(pool) 132 - .await 133 - .expect("User not found"); 134 - assert!(user.password_reset_code.is_none()); 135 - assert!(user.password_reset_code_expires_at.is_none()); 126 + let info = repos 127 + .user 128 + .get_password_reset_info(&email) 129 + .await 130 + .expect("failed to look up user") 131 + .expect("user not found"); 132 + assert!(info.code.is_none()); 133 + assert!(info.expires_at.is_none()); 136 134 let res = client 137 135 .post(format!( 138 136 "{}/xrpc/com.atproto.server.createSession", ··· 186 184 async fn test_reset_password_with_expired_token() { 187 185 let client = common::client(); 188 186 let base_url = common::base_url().await; 189 - let pool = common::get_test_db_pool().await; 187 + let repos = common::get_test_repos().await; 190 188 let handle = format!("pr3{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 191 189 let email = format!("{}@example.com", handle); 192 190 let payload = json!({ ··· 214 212 .await 215 213 .expect("Failed to request password reset"); 216 214 assert_eq!(res.status(), StatusCode::OK); 217 - let user = sqlx::query!( 218 - "SELECT password_reset_code FROM users WHERE email = $1", 219 - email 220 - ) 221 - .fetch_one(pool) 222 - .await 223 - .expect("User not found"); 224 - let token = user.password_reset_code.expect("No reset code"); 225 - sqlx::query!( 226 - "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1", 227 - email 228 - ) 229 - .execute(pool) 230 - .await 231 - .expect("Failed to expire token"); 215 + let info = repos 216 + .user 217 + .get_password_reset_info(&email) 218 + .await 219 + .expect("failed to look up user") 220 + .expect("user not found"); 221 + let token = info.code.expect("No reset code"); 222 + repos 223 + .user 224 + .expire_password_reset_code(&email) 225 + .await 226 + .expect("Failed to expire token"); 232 227 let res = client 233 228 .post(format!( 234 229 "{}/xrpc/com.atproto.server.resetPassword", ··· 250 245 async fn test_reset_password_invalidates_sessions() { 251 246 let client = common::client(); 252 247 let base_url = common::base_url().await; 253 - let pool = common::get_test_db_pool().await; 248 + let repos = common::get_test_repos().await; 254 249 let handle = format!("pr4{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 255 250 let email = format!("{}@example.com", handle); 256 251 let payload = json!({ ··· 288 283 .await 289 284 .expect("Failed to request password reset"); 290 285 assert_eq!(res.status(), StatusCode::OK); 291 - let user = sqlx::query!( 292 - "SELECT password_reset_code FROM users WHERE email = $1", 293 - email 294 - ) 295 - .fetch_one(pool) 296 - .await 297 - .expect("User not found"); 298 - let token = user.password_reset_code.expect("No reset code"); 286 + let info = repos 287 + .user 288 + .get_password_reset_info(&email) 289 + .await 290 + .expect("failed to look up user") 291 + .expect("user not found"); 292 + let token = info.code.expect("No reset code"); 299 293 let res = client 300 294 .post(format!( 301 295 "{}/xrpc/com.atproto.server.resetPassword", ··· 338 332 339 333 #[tokio::test] 340 334 async fn test_reset_password_creates_notification() { 341 - let pool = common::get_test_db_pool().await; 335 + let repos = common::get_test_repos().await; 342 336 let client = common::client(); 343 337 let base_url = common::base_url().await; 344 338 let handle = format!("pr5{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); ··· 358 352 .await 359 353 .expect("Failed to create account"); 360 354 assert_eq!(res.status(), StatusCode::OK); 361 - let user = sqlx::query!("SELECT id FROM users WHERE email = $1", email) 362 - .fetch_one(pool) 355 + let user = repos 356 + .user 357 + .get_by_email(&email) 363 358 .await 364 - .expect("User not found"); 365 - let initial_count: i64 = sqlx::query_scalar!( 366 - "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'", 367 - user.id 368 - ) 369 - .fetch_one(pool) 370 - .await 371 - .expect("Failed to count") 372 - .unwrap_or(0); 359 + .expect("failed to look up user") 360 + .expect("user not found"); 361 + let initial_count = repos 362 + .infra 363 + .count_comms_by_type(user.id, CommsType::PasswordReset) 364 + .await 365 + .expect("Failed to count"); 373 366 let res = client 374 367 .post(format!( 375 368 "{}/xrpc/com.atproto.server.requestPasswordReset", ··· 380 373 .await 381 374 .expect("Failed to request password reset"); 382 375 assert_eq!(res.status(), StatusCode::OK); 383 - let final_count: i64 = sqlx::query_scalar!( 384 - "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'", 385 - user.id 386 - ) 387 - .fetch_one(pool) 388 - .await 389 - .expect("Failed to count") 390 - .unwrap_or(0); 376 + let final_count = repos 377 + .infra 378 + .count_comms_by_type(user.id, CommsType::PasswordReset) 379 + .await 380 + .expect("Failed to count"); 391 381 assert_eq!(final_count - initial_count, 1); 392 382 }
+15 -34
crates/tranquil-pds/tests/plc_migration.rs
··· 3 3 use k256::ecdsa::SigningKey; 4 4 use reqwest::StatusCode; 5 5 use serde_json::{Value, json}; 6 - use sqlx::PgPool; 6 + use tranquil_types::Did; 7 7 use wiremock::matchers::{method, path}; 8 8 use wiremock::{Mock, MockServer, ResponseTemplate}; 9 9 ··· 36 36 } 37 37 38 38 async fn get_user_signing_key(did: &str) -> Option<Vec<u8>> { 39 - let db_url = get_db_connection_string().await; 40 - let pool = PgPool::connect(&db_url).await.ok()?; 41 - let row = sqlx::query!( 42 - r#" 43 - SELECT k.key_bytes, k.encryption_version 44 - FROM user_keys k 45 - JOIN users u ON k.user_id = u.id 46 - WHERE u.did = $1 47 - "#, 48 - did 49 - ) 50 - .fetch_optional(&pool) 51 - .await 52 - .ok()??; 53 - tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version).ok() 39 + let repos = get_test_repos().await; 40 + let parsed_did = Did::new(did.to_string()).ok()?; 41 + let key_info = repos.user.get_user_key_by_did(&parsed_did).await.ok()??; 42 + tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version).ok() 54 43 } 55 44 56 45 async fn get_plc_token_from_db(did: &str) -> Option<String> { 57 - let db_url = get_db_connection_string().await; 58 - let pool = PgPool::connect(&db_url).await.ok()?; 59 - sqlx::query_scalar!( 60 - r#" 61 - SELECT t.token 62 - FROM plc_operation_tokens t 63 - JOIN users u ON t.user_id = u.id 64 - WHERE u.did = $1 65 - "#, 66 - did 67 - ) 68 - .fetch_optional(&pool) 69 - .await 70 - .ok()? 46 + let repos = get_test_repos().await; 47 + let parsed_did = Did::new(did.to_string()).ok()?; 48 + let tokens = repos.infra.get_plc_tokens_by_did(&parsed_did).await.ok()?; 49 + tokens.into_iter().next().map(|t| t.token) 71 50 } 72 51 73 52 async fn get_user_handle(did: &str) -> Option<String> { 74 - let db_url = get_db_connection_string().await; 75 - let pool = PgPool::connect(&db_url).await.ok()?; 76 - sqlx::query_scalar!(r#"SELECT handle FROM users WHERE did = $1"#, did) 77 - .fetch_optional(&pool) 53 + let repos = get_test_repos().await; 54 + let parsed_did = Did::new(did.to_string()).ok()?; 55 + repos 56 + .user 57 + .get_handle_by_did(&parsed_did) 78 58 .await 79 59 .ok()? 60 + .map(|h| h.to_string()) 80 61 } 81 62 82 63 fn create_mock_last_op(
+37 -21
crates/tranquil-pds/tests/plc_operations.rs
··· 2 2 use common::*; 3 3 use reqwest::StatusCode; 4 4 use serde_json::json; 5 - use sqlx::PgPool; 5 + use tranquil_types::Did; 6 6 7 7 #[tokio::test] 8 8 async fn test_plc_operation_auth() { ··· 176 176 .await 177 177 .unwrap(); 178 178 assert_eq!(res.status(), StatusCode::OK); 179 - let db_url = get_db_connection_string().await; 180 - let pool = PgPool::connect(&db_url).await.unwrap(); 181 - let row = sqlx::query!( 182 - "SELECT t.token, t.expires_at FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", 183 - did 184 - ).fetch_optional(&pool).await.unwrap(); 185 - assert!(row.is_some(), "PLC token should be created in database"); 186 - let row = row.unwrap(); 187 - assert_eq!(row.token.len(), 11, "Token should be in format xxxxx-xxxxx"); 188 - assert!(row.token.contains('-'), "Token should contain hyphen"); 179 + let repos = get_test_repos().await; 180 + let parsed_did = Did::new(did.clone()).unwrap(); 181 + let tokens = repos 182 + .infra 183 + .get_plc_tokens_by_did(&parsed_did) 184 + .await 185 + .unwrap(); 189 186 assert!( 190 - row.expires_at > chrono::Utc::now(), 187 + !tokens.is_empty(), 188 + "PLC token should be created in database" 189 + ); 190 + let first = &tokens[0]; 191 + assert_eq!( 192 + first.token.len(), 193 + 11, 194 + "Token should be in format xxxxx-xxxxx" 195 + ); 196 + assert!(first.token.contains('-'), "Token should contain hyphen"); 197 + assert!( 198 + first.expires_at > chrono::Utc::now(), 191 199 "Token should not be expired" 192 200 ); 193 - let diff = row.expires_at - chrono::Utc::now(); 201 + let diff = first.expires_at - chrono::Utc::now(); 194 202 assert!( 195 203 diff.num_minutes() >= 9 && diff.num_minutes() <= 11, 196 204 "Token should expire in ~10 minutes" 197 205 ); 198 - let token1 = row.token.clone(); 206 + let token1 = first.token.clone(); 199 207 let res = client 200 208 .post(format!( 201 209 "{}/xrpc/com.atproto.identity.requestPlcOperationSignature", ··· 206 214 .await 207 215 .unwrap(); 208 216 assert_eq!(res.status(), StatusCode::OK); 209 - let token2 = sqlx::query_scalar!( 210 - "SELECT t.token FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", did 211 - ).fetch_one(&pool).await.unwrap(); 212 - assert_ne!(token1, token2, "Second request should generate a new token"); 213 - let count: i64 = sqlx::query_scalar!( 214 - "SELECT COUNT(*) as \"count!\" FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1", did 215 - ).fetch_one(&pool).await.unwrap(); 217 + let tokens2 = repos 218 + .infra 219 + .get_plc_tokens_by_did(&parsed_did) 220 + .await 221 + .unwrap(); 222 + let token2 = &tokens2[0].token; 223 + assert_ne!( 224 + token1, *token2, 225 + "Second request should generate a new token" 226 + ); 227 + let count = repos 228 + .infra 229 + .count_plc_tokens_by_did(&parsed_did) 230 + .await 231 + .unwrap(); 216 232 assert_eq!(count, 1, "Should only have one token per user"); 217 233 }
+16 -41
crates/tranquil-pds/tests/repo_lifecycle.rs
··· 58 58 let client = client(); 59 59 let (token, did) = create_account_and_login(&client).await; 60 60 61 - let pool = get_test_db_pool().await; 62 - let cursor: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 63 - .fetch_one(pool) 64 - .await 65 - .unwrap(); 61 + let repos = get_test_repos().await; 62 + let cursor = repos.repo.get_max_seq().await.unwrap().as_i64(); 66 63 let consumer = FirehoseConsumer::connect_with_cursor(app_port(), cursor).await; 67 64 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 68 65 ··· 136 133 let v1_cid_str = v1_body["cid"].as_str().unwrap(); 137 134 let v1_cid = Cid::from_str(v1_cid_str).unwrap(); 138 135 139 - let pool = get_test_db_pool().await; 140 - let cursor: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 141 - .fetch_one(pool) 142 - .await 143 - .unwrap(); 136 + let repos = get_test_repos().await; 137 + let cursor = repos.repo.get_max_seq().await.unwrap().as_i64(); 144 138 let consumer = FirehoseConsumer::connect_with_cursor(app_port(), cursor).await; 145 139 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 146 140 ··· 208 202 let collection = parts[parts.len() - 2]; 209 203 let rkey = parts[parts.len() - 1]; 210 204 211 - let pool = get_test_db_pool().await; 212 - let cursor: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 213 - .fetch_one(pool) 214 - .await 215 - .unwrap(); 205 + let repos = get_test_repos().await; 206 + let cursor = repos.repo.get_max_seq().await.unwrap().as_i64(); 216 207 let consumer = FirehoseConsumer::connect_with_cursor(app_port(), cursor).await; 217 208 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 218 209 ··· 254 245 let client = client(); 255 246 let (token, did) = create_account_and_login(&client).await; 256 247 257 - let pool = get_test_db_pool().await; 258 - let cursor: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 259 - .fetch_one(pool) 260 - .await 261 - .unwrap(); 248 + let repos = get_test_repos().await; 249 + let cursor = repos.repo.get_max_seq().await.unwrap().as_i64(); 262 250 let consumer = FirehoseConsumer::connect_with_cursor(app_port(), cursor).await; 263 251 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 264 252 ··· 326 314 let client = client(); 327 315 let (token, did) = create_account_and_login(&client).await; 328 316 329 - let pool = get_test_db_pool().await; 330 - let cursor: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 331 - .fetch_one(pool) 332 - .await 333 - .unwrap(); 317 + let repos = get_test_repos().await; 318 + let cursor = repos.repo.get_max_seq().await.unwrap().as_i64(); 334 319 let consumer = FirehoseConsumer::connect_with_cursor(app_port(), cursor).await; 335 320 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 336 321 ··· 410 395 bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()), 411 396 }; 412 397 413 - let pool = get_test_db_pool().await; 414 - let cursor: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 415 - .fetch_one(pool) 416 - .await 417 - .unwrap(); 398 + let repos = get_test_repos().await; 399 + let cursor = repos.repo.get_max_seq().await.unwrap().as_i64(); 418 400 let consumer = FirehoseConsumer::connect_with_cursor(app_port(), cursor).await; 419 401 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 420 402 ··· 461 443 let client = client(); 462 444 let (token, did) = create_account_and_login(&client).await; 463 445 464 - let pool = get_test_db_pool().await; 465 - let baseline_seq: i64 = 466 - sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 467 - .fetch_one(pool) 468 - .await 469 - .unwrap(); 446 + let repos = get_test_repos().await; 447 + let baseline_seq = repos.repo.get_max_seq().await.unwrap().as_i64(); 470 448 471 449 let mut expected_cids: Vec<String> = Vec::with_capacity(5); 472 450 let texts = [ ··· 517 495 let (alice_token, alice_did) = create_account_and_login(&client).await; 518 496 let (bob_token, bob_did) = create_account_and_login(&client).await; 519 497 520 - let pool = get_test_db_pool().await; 521 - let cursor: i64 = sqlx::query_scalar::<_, i64>("SELECT COALESCE(MAX(seq), 0) FROM repo_seq") 522 - .fetch_one(pool) 523 - .await 524 - .unwrap(); 498 + let repos = get_test_repos().await; 499 + let cursor = repos.repo.get_max_seq().await.unwrap().as_i64(); 525 500 let consumer = FirehoseConsumer::connect_with_cursor(app_port(), cursor).await; 526 501 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 527 502
+32 -16
crates/tranquil-pds/tests/ripple_cluster.rs
··· 97 97 .expect("no accessJwt") 98 98 .to_string(); 99 99 100 - let pool = common::get_test_db_pool().await; 101 - let body_text: String = sqlx::query_scalar!( 102 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 103 - &did 104 - ) 105 - .fetch_one(pool) 106 - .await 107 - .expect("verification code not found"); 100 + let repos = common::get_test_repos().await; 101 + let user = repos 102 + .user 103 + .get_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 104 + .await 105 + .expect("failed to look up user") 106 + .expect("user not found"); 107 + let comms = repos 108 + .infra 109 + .get_latest_comms_for_user(user.id, tranquil_db_traits::CommsType::EmailVerification, 1) 110 + .await 111 + .expect("failed to get comms"); 112 + let body_text = comms 113 + .first() 114 + .map(|c| c.body.clone()) 115 + .expect("no email_verification comms found"); 108 116 109 117 let lines: Vec<&str> = body_text.lines().collect(); 110 118 let verification_code = lines ··· 624 632 .expect("no accessJwt") 625 633 .to_string(); 626 634 627 - let pool = common::get_test_db_pool().await; 628 - let body_text: String = sqlx::query_scalar!( 629 - "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 630 - &did 631 - ) 632 - .fetch_one(pool) 633 - .await 634 - .expect("verification code not found"); 635 + let repos = common::get_test_repos().await; 636 + let user = repos 637 + .user 638 + .get_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 639 + .await 640 + .expect("failed to look up user") 641 + .expect("user not found"); 642 + let comms = repos 643 + .infra 644 + .get_latest_comms_for_user(user.id, tranquil_db_traits::CommsType::EmailVerification, 1) 645 + .await 646 + .expect("failed to get comms"); 647 + let body_text = comms 648 + .first() 649 + .map(|c| c.body.clone()) 650 + .expect("no email_verification comms found"); 635 651 636 652 let lines: Vec<&str> = body_text.lines().collect(); 637 653 let verification_code = lines
+22 -25
crates/tranquil-pds/tests/signing_key.rs
··· 31 31 async fn test_reserve_signing_key_with_did() { 32 32 let client = common::client(); 33 33 let base_url = common::base_url().await; 34 - let pool = common::get_test_db_pool().await; 34 + let repos = common::get_test_repos().await; 35 35 let target_did = "did:plc:test123456"; 36 36 let res = client 37 37 .post(format!( ··· 46 46 let body: Value = res.json().await.expect("Response was not valid JSON"); 47 47 let signing_key = body["signingKey"].as_str().unwrap(); 48 48 assert!(signing_key.starts_with("did:key:z")); 49 - let row = sqlx::query!( 50 - "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1", 51 - signing_key 52 - ) 53 - .fetch_one(pool) 54 - .await 55 - .expect("Reserved key not found in database"); 56 - assert_eq!(row.did.as_deref(), Some(target_did)); 49 + let row = repos 50 + .infra 51 + .get_reserved_signing_key_full(signing_key) 52 + .await 53 + .expect("db error") 54 + .expect("Reserved key not found in database"); 55 + assert_eq!(row.did.as_ref().map(|d| d.as_str()), Some(target_did)); 57 56 assert_eq!(row.public_key_did_key, signing_key); 58 57 } 59 58 ··· 61 60 async fn test_reserve_signing_key_stores_private_key() { 62 61 let client = common::client(); 63 62 let base_url = common::base_url().await; 64 - let pool = common::get_test_db_pool().await; 63 + let repos = common::get_test_repos().await; 65 64 let res = client 66 65 .post(format!( 67 66 "{}/xrpc/com.atproto.server.reserveSigningKey", ··· 74 73 assert_eq!(res.status(), StatusCode::OK); 75 74 let body: Value = res.json().await.expect("Response was not valid JSON"); 76 75 let signing_key = body["signingKey"].as_str().unwrap(); 77 - let row = sqlx::query!( 78 - "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", 79 - signing_key 80 - ) 81 - .fetch_one(pool) 82 - .await 83 - .expect("Reserved key not found in database"); 76 + let row = repos 77 + .infra 78 + .get_reserved_signing_key_full(signing_key) 79 + .await 80 + .expect("db error") 81 + .expect("Reserved key not found in database"); 84 82 assert_eq!( 85 83 row.private_key_bytes.len(), 86 84 32, ··· 151 149 async fn test_create_account_with_reserved_signing_key() { 152 150 let client = common::client(); 153 151 let base_url = common::base_url().await; 154 - let pool = common::get_test_db_pool().await; 152 + let repos = common::get_test_repos().await; 155 153 let res = client 156 154 .post(format!( 157 155 "{}/xrpc/com.atproto.server.reserveSigningKey", ··· 185 183 let did = body["did"].as_str().unwrap(); 186 184 let access_jwt = verify_new_account(&client, did).await; 187 185 assert!(!access_jwt.is_empty()); 188 - let reserved = sqlx::query!( 189 - "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1", 190 - signing_key 191 - ) 192 - .fetch_one(pool) 193 - .await 194 - .expect("Reserved key not found"); 186 + let reserved = repos 187 + .infra 188 + .get_reserved_signing_key_full(signing_key) 189 + .await 190 + .expect("db error") 191 + .expect("Reserved key not found"); 195 192 assert!( 196 193 reserved.used_at.is_some(), 197 194 "Reserved key should be marked as used"
+304 -565
crates/tranquil-pds/tests/sso.rs
··· 1 1 mod common; 2 2 3 - use common::{base_url, client, create_account_and_login, get_test_db_pool}; 3 + use common::{base_url, client, create_account_and_login, get_test_repos}; 4 4 use reqwest::StatusCode; 5 5 use serde_json::{Value, json}; 6 - use tranquil_db_traits::SsoProviderType; 7 - use tranquil_types::Did; 6 + use tranquil_db_traits::{CommsChannel, SsoAction, SsoProviderType}; 7 + use tranquil_oauth::{ 8 + AuthorizationRequestParameters, CodeChallengeMethod, RequestData, ResponseType, 9 + }; 10 + use tranquil_types::{Did, RequestId}; 8 11 9 12 #[tokio::test] 10 13 async fn test_sso_providers_endpoint() { ··· 226 229 #[tokio::test] 227 230 async fn test_external_identity_repository_crud() { 228 231 let _url = base_url().await; 229 - let pool = get_test_db_pool().await; 232 + let repos = get_test_repos().await; 233 + let client = client(); 230 234 231 - let did: Did = format!( 232 - "did:plc:test{}", 233 - &uuid::Uuid::new_v4().simple().to_string()[..12] 234 - ) 235 - .parse() 236 - .expect("valid test DID"); 235 + let (_token, did_string) = create_account_and_login(&client).await; 236 + let did: Did = did_string.parse().expect("valid DID"); 237 + 237 238 let provider = SsoProviderType::Github; 238 239 let provider_user_id = format!("github_user_{}", uuid::Uuid::new_v4().simple()); 239 240 240 - sqlx::query!( 241 - "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')", 242 - did.as_str(), 243 - format!("test{}", &uuid::Uuid::new_v4().simple().to_string()[..8]), 244 - format!( 245 - "test{}@example.com", 246 - &uuid::Uuid::new_v4().simple().to_string()[..8] 241 + let id = repos 242 + .sso 243 + .create_external_identity( 244 + &did, 245 + provider, 246 + &provider_user_id, 247 + Some("testuser"), 248 + Some("test@github.com"), 247 249 ) 248 - ) 249 - .execute(pool) 250 - .await 251 - .unwrap(); 250 + .await 251 + .unwrap(); 252 252 253 - let id: uuid::Uuid = sqlx::query_scalar!( 254 - r#" 255 - INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email) 256 - VALUES ($1, $2, $3, $4, $5) 257 - RETURNING id 258 - "#, 259 - did.as_str(), 260 - provider as SsoProviderType, 261 - &provider_user_id, 262 - Some("testuser"), 263 - Some("test@github.com"), 264 - ) 265 - .fetch_one(pool) 266 - .await 267 - .unwrap(); 268 - 269 - let found = sqlx::query!( 270 - r#" 271 - SELECT id, did, provider as "provider: SsoProviderType", provider_user_id, provider_username, provider_email 272 - FROM external_identities 273 - WHERE provider = $1 AND provider_user_id = $2 274 - "#, 275 - provider as SsoProviderType, 276 - &provider_user_id, 277 - ) 278 - .fetch_optional(pool) 279 - .await 280 - .unwrap(); 253 + let found = repos 254 + .sso 255 + .get_external_identity_by_provider(provider, &provider_user_id) 256 + .await 257 + .unwrap(); 281 258 282 259 assert!(found.is_some()); 283 260 let found = found.unwrap(); 284 261 assert_eq!(found.id, id); 285 - assert_eq!(found.did, did.as_str()); 286 - assert_eq!(found.provider_username, Some("testuser".to_string())); 262 + assert_eq!(found.did, did); 263 + assert_eq!( 264 + found.provider_username.as_ref().unwrap().as_str(), 265 + "testuser" 266 + ); 287 267 288 - let identities = sqlx::query!( 289 - r#" 290 - SELECT id FROM external_identities WHERE did = $1 291 - "#, 292 - did.as_str(), 293 - ) 294 - .fetch_all(pool) 295 - .await 296 - .unwrap(); 268 + let identities = repos 269 + .sso 270 + .get_external_identities_by_did(&did) 271 + .await 272 + .unwrap(); 297 273 298 274 assert_eq!(identities.len(), 1); 299 275 300 - sqlx::query!( 301 - r#" 302 - UPDATE external_identities 303 - SET provider_username = $2, last_login_at = NOW() 304 - WHERE id = $1 305 - "#, 306 - id, 307 - "updated_username", 308 - ) 309 - .execute(pool) 310 - .await 311 - .unwrap(); 276 + repos 277 + .sso 278 + .update_external_identity_login(id, Some("updated_username"), None) 279 + .await 280 + .unwrap(); 312 281 313 - let updated = sqlx::query!( 314 - r#"SELECT provider_username, last_login_at FROM external_identities WHERE id = $1"#, 315 - id, 316 - ) 317 - .fetch_one(pool) 318 - .await 319 - .unwrap(); 282 + let updated = repos 283 + .sso 284 + .get_external_identity_by_provider(provider, &provider_user_id) 285 + .await 286 + .unwrap() 287 + .unwrap(); 320 288 321 289 assert_eq!( 322 - updated.provider_username, 323 - Some("updated_username".to_string()) 290 + updated.provider_username.as_ref().unwrap().as_str(), 291 + "updated_username" 324 292 ); 325 293 assert!(updated.last_login_at.is_some()); 326 294 327 - let deleted = sqlx::query!( 328 - r#"DELETE FROM external_identities WHERE id = $1 AND did = $2"#, 329 - id, 330 - did.as_str(), 331 - ) 332 - .execute(pool) 333 - .await 334 - .unwrap(); 335 - 336 - assert_eq!(deleted.rows_affected(), 1); 295 + let deleted = repos.sso.delete_external_identity(id, &did).await.unwrap(); 296 + assert!(deleted); 337 297 338 - let not_found = sqlx::query!(r#"SELECT id FROM external_identities WHERE id = $1"#, id,) 339 - .fetch_optional(pool) 298 + let not_found = repos 299 + .sso 300 + .get_external_identity_by_provider(provider, &provider_user_id) 340 301 .await 341 302 .unwrap(); 342 303 ··· 346 307 #[tokio::test] 347 308 async fn test_external_identity_unique_constraints() { 348 309 let _url = base_url().await; 349 - let pool = get_test_db_pool().await; 310 + let repos = get_test_repos().await; 311 + let client = client(); 350 312 351 - let did1: Did = format!( 352 - "did:plc:uc1{}", 353 - &uuid::Uuid::new_v4().simple().to_string()[..10] 354 - ) 355 - .parse() 356 - .expect("valid test DID"); 357 - let did2: Did = format!( 358 - "did:plc:uc2{}", 359 - &uuid::Uuid::new_v4().simple().to_string()[..10] 360 - ) 361 - .parse() 362 - .expect("valid test DID"); 313 + let (_token1, did1_string) = create_account_and_login(&client).await; 314 + let did1: Did = did1_string.parse().expect("valid DID"); 315 + let (_token2, did2_string) = create_account_and_login(&client).await; 316 + let did2: Did = did2_string.parse().expect("valid DID"); 317 + 363 318 let provider_user_id = format!("unique_test_{}", uuid::Uuid::new_v4().simple()); 364 319 365 - sqlx::query!( 366 - "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')", 367 - did1.as_str(), 368 - format!("uc1{}", &uuid::Uuid::new_v4().simple().to_string()[..8]), 369 - format!( 370 - "uc1{}@example.com", 371 - &uuid::Uuid::new_v4().simple().to_string()[..8] 320 + repos 321 + .sso 322 + .create_external_identity( 323 + &did1, 324 + SsoProviderType::Github, 325 + &provider_user_id, 326 + None, 327 + None, 372 328 ) 373 - ) 374 - .execute(pool) 375 - .await 376 - .unwrap(); 329 + .await 330 + .unwrap(); 377 331 378 - sqlx::query!( 379 - "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')", 380 - did2.as_str(), 381 - format!("uc2{}", &uuid::Uuid::new_v4().simple().to_string()[..8]), 382 - format!( 383 - "uc2{}@example.com", 384 - &uuid::Uuid::new_v4().simple().to_string()[..8] 332 + let duplicate_provider_user = repos 333 + .sso 334 + .create_external_identity( 335 + &did2, 336 + SsoProviderType::Github, 337 + &provider_user_id, 338 + None, 339 + None, 385 340 ) 386 - ) 387 - .execute(pool) 388 - .await 389 - .unwrap(); 390 - 391 - sqlx::query!( 392 - r#" 393 - INSERT INTO external_identities (did, provider, provider_user_id) 394 - VALUES ($1, $2, $3) 395 - "#, 396 - did1.as_str(), 397 - SsoProviderType::Github as SsoProviderType, 398 - &provider_user_id, 399 - ) 400 - .execute(pool) 401 - .await 402 - .unwrap(); 403 - 404 - let duplicate_provider_user = sqlx::query!( 405 - r#" 406 - INSERT INTO external_identities (did, provider, provider_user_id) 407 - VALUES ($1, $2, $3) 408 - "#, 409 - did2.as_str(), 410 - SsoProviderType::Github as SsoProviderType, 411 - &provider_user_id, 412 - ) 413 - .execute(pool) 414 - .await; 341 + .await; 415 342 416 343 assert!(duplicate_provider_user.is_err()); 417 344 418 - let duplicate_did_provider = sqlx::query!( 419 - r#" 420 - INSERT INTO external_identities (did, provider, provider_user_id) 421 - VALUES ($1, $2, $3) 422 - "#, 423 - did1.as_str(), 424 - SsoProviderType::Github as SsoProviderType, 425 - "different_user_id", 426 - ) 427 - .execute(pool) 428 - .await; 345 + let duplicate_did_provider = repos 346 + .sso 347 + .create_external_identity( 348 + &did1, 349 + SsoProviderType::Github, 350 + "different_user_id", 351 + None, 352 + None, 353 + ) 354 + .await; 429 355 430 356 assert!(duplicate_did_provider.is_err()); 431 357 432 358 let discord_user_id = format!("discord_user_{}", uuid::Uuid::new_v4().simple()); 433 - let different_provider = sqlx::query!( 434 - r#" 435 - INSERT INTO external_identities (did, provider, provider_user_id) 436 - VALUES ($1, $2, $3) 437 - "#, 438 - did1.as_str(), 439 - SsoProviderType::Discord as SsoProviderType, 440 - &discord_user_id, 441 - ) 442 - .execute(pool) 443 - .await; 359 + let different_provider = repos 360 + .sso 361 + .create_external_identity( 362 + &did1, 363 + SsoProviderType::Discord, 364 + &discord_user_id, 365 + None, 366 + None, 367 + ) 368 + .await; 444 369 445 370 assert!( 446 371 different_provider.is_ok(), ··· 452 377 #[tokio::test] 453 378 async fn test_sso_auth_state_lifecycle() { 454 379 let _url = base_url().await; 455 - let pool = get_test_db_pool().await; 380 + let repos = get_test_repos().await; 456 381 457 382 let state = format!("test_state_{}", uuid::Uuid::new_v4().simple()); 458 383 let request_uri = "urn:ietf:params:oauth:request_uri:test123"; 459 384 460 - sqlx::query!( 461 - r#" 462 - INSERT INTO sso_auth_state (state, request_uri, provider, action, nonce, code_verifier) 463 - VALUES ($1, $2, $3, $4, $5, $6) 464 - "#, 465 - &state, 466 - request_uri, 467 - SsoProviderType::Github as SsoProviderType, 468 - "login", 469 - Some("test_nonce"), 470 - Some("test_verifier"), 471 - ) 472 - .execute(pool) 473 - .await 474 - .unwrap(); 475 - 476 - let found = sqlx::query!( 477 - r#" 478 - SELECT state, request_uri, provider as "provider: SsoProviderType", action, nonce, code_verifier 479 - FROM sso_auth_state 480 - WHERE state = $1 481 - "#, 482 - &state, 483 - ) 484 - .fetch_optional(pool) 485 - .await 486 - .unwrap(); 385 + repos 386 + .sso 387 + .create_sso_auth_state( 388 + &state, 389 + request_uri, 390 + SsoProviderType::Github, 391 + SsoAction::Login, 392 + Some("test_nonce"), 393 + Some("test_verifier"), 394 + None, 395 + ) 396 + .await 397 + .unwrap(); 487 398 488 - assert!(found.is_some()); 489 - let found = found.unwrap(); 490 - assert_eq!(found.request_uri, request_uri); 491 - assert_eq!(found.action, "login"); 492 - assert_eq!(found.nonce, Some("test_nonce".to_string())); 493 - assert_eq!(found.code_verifier, Some("test_verifier".to_string())); 494 - 495 - let consumed = sqlx::query!( 496 - r#" 497 - DELETE FROM sso_auth_state 498 - WHERE state = $1 AND expires_at > NOW() 499 - RETURNING state, request_uri 500 - "#, 501 - &state, 502 - ) 503 - .fetch_optional(pool) 504 - .await 505 - .unwrap(); 399 + let consumed = repos.sso.consume_sso_auth_state(&state).await.unwrap(); 506 400 507 401 assert!(consumed.is_some()); 508 - 509 - let not_found = sqlx::query!( 510 - r#"SELECT state FROM sso_auth_state WHERE state = $1"#, 511 - &state, 512 - ) 513 - .fetch_optional(pool) 514 - .await 515 - .unwrap(); 516 - 517 - assert!(not_found.is_none()); 518 - 519 - let double_consume = sqlx::query!( 520 - r#" 521 - DELETE FROM sso_auth_state 522 - WHERE state = $1 AND expires_at > NOW() 523 - RETURNING state 524 - "#, 525 - &state, 526 - ) 527 - .fetch_optional(pool) 528 - .await 529 - .unwrap(); 402 + let consumed = consumed.unwrap(); 403 + assert_eq!(consumed.request_uri, request_uri); 404 + assert_eq!(consumed.action, SsoAction::Login); 405 + assert_eq!(consumed.nonce.as_deref(), Some("test_nonce")); 406 + assert_eq!(consumed.code_verifier.as_deref(), Some("test_verifier")); 530 407 408 + let double_consume = repos.sso.consume_sso_auth_state(&state).await.unwrap(); 531 409 assert!(double_consume.is_none()); 532 410 } 533 411 534 412 #[tokio::test] 535 413 async fn test_sso_auth_state_expiration() { 536 414 let _url = base_url().await; 537 - let pool = get_test_db_pool().await; 538 - 539 - let state = format!("expired_state_{}", uuid::Uuid::new_v4().simple()); 540 - 541 - sqlx::query!( 542 - r#" 543 - INSERT INTO sso_auth_state (state, request_uri, provider, action, expires_at) 544 - VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour') 545 - "#, 546 - &state, 547 - "urn:test:expired", 548 - SsoProviderType::Github as SsoProviderType, 549 - "login", 550 - ) 551 - .execute(pool) 552 - .await 553 - .unwrap(); 415 + let repos = get_test_repos().await; 554 416 555 - let consumed = sqlx::query!( 556 - r#" 557 - DELETE FROM sso_auth_state 558 - WHERE state = $1 AND expires_at > NOW() 559 - RETURNING state 560 - "#, 561 - &state, 562 - ) 563 - .fetch_optional(pool) 564 - .await 565 - .unwrap(); 417 + let consumed = repos 418 + .sso 419 + .consume_sso_auth_state("nonexistent_state_token") 420 + .await 421 + .unwrap(); 566 422 567 423 assert!(consumed.is_none()); 568 424 569 - let cleaned = sqlx::query!(r#"DELETE FROM sso_auth_state WHERE expires_at < NOW()"#,) 570 - .execute(pool) 571 - .await 572 - .unwrap(); 425 + let cleaned = repos.sso.cleanup_expired_sso_auth_states().await.unwrap(); 573 426 574 - assert!(cleaned.rows_affected() >= 1); 427 + assert!(cleaned == 0 || cleaned >= 1); 575 428 } 576 429 577 430 #[tokio::test] 578 431 async fn test_delete_external_identity_wrong_did() { 579 432 let _url = base_url().await; 580 - let pool = get_test_db_pool().await; 433 + let repos = get_test_repos().await; 434 + let client = client(); 581 435 582 - let did: Did = format!( 583 - "did:plc:del{}", 584 - &uuid::Uuid::new_v4().simple().to_string()[..10] 585 - ) 586 - .parse() 587 - .expect("valid test DID"); 436 + let (_token, did_string) = create_account_and_login(&client).await; 437 + let did: Did = did_string.parse().expect("valid DID"); 588 438 let wrong_did: Did = "did:plc:wrongdid12345".parse().expect("valid test DID"); 589 439 590 - sqlx::query!( 591 - "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')", 592 - did.as_str(), 593 - format!("del{}", &uuid::Uuid::new_v4().simple().to_string()[..8]), 594 - format!( 595 - "del{}@example.com", 596 - &uuid::Uuid::new_v4().simple().to_string()[..8] 597 - ) 598 - ) 599 - .execute(pool) 600 - .await 601 - .unwrap(); 440 + let provider_user_id = format!("delete_test_{}", uuid::Uuid::new_v4().simple()); 602 441 603 - let id: uuid::Uuid = sqlx::query_scalar!( 604 - r#" 605 - INSERT INTO external_identities (did, provider, provider_user_id) 606 - VALUES ($1, $2, $3) 607 - RETURNING id 608 - "#, 609 - did.as_str(), 610 - SsoProviderType::Github as SsoProviderType, 611 - format!("delete_test_{}", uuid::Uuid::new_v4().simple()), 612 - ) 613 - .fetch_one(pool) 614 - .await 615 - .unwrap(); 442 + let id = repos 443 + .sso 444 + .create_external_identity(&did, SsoProviderType::Github, &provider_user_id, None, None) 445 + .await 446 + .unwrap(); 616 447 617 - let wrong_delete = sqlx::query!( 618 - r#"DELETE FROM external_identities WHERE id = $1 AND did = $2"#, 619 - id, 620 - wrong_did.as_str(), 621 - ) 622 - .execute(pool) 623 - .await 624 - .unwrap(); 448 + let deleted = repos 449 + .sso 450 + .delete_external_identity(id, &wrong_did) 451 + .await 452 + .unwrap(); 625 453 626 - assert_eq!(wrong_delete.rows_affected(), 0); 454 + assert!(!deleted); 627 455 628 - let still_exists = sqlx::query!(r#"SELECT id FROM external_identities WHERE id = $1"#, id,) 629 - .fetch_optional(pool) 456 + let still_exists = repos 457 + .sso 458 + .get_external_identity_by_provider(SsoProviderType::Github, &provider_user_id) 630 459 .await 631 460 .unwrap(); 632 461 ··· 636 465 #[tokio::test] 637 466 async fn test_sso_pending_registration_lifecycle() { 638 467 let _url = base_url().await; 639 - let pool = get_test_db_pool().await; 468 + let repos = get_test_repos().await; 640 469 641 470 let token = format!("pending_token_{}", uuid::Uuid::new_v4().simple()); 642 471 let request_uri = "urn:ietf:params:oauth:request_uri:pendingtest"; 643 472 let provider_user_id = format!("pending_user_{}", uuid::Uuid::new_v4().simple()); 644 473 645 - sqlx::query!( 646 - r#" 647 - INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email) 648 - VALUES ($1, $2, $3, $4, $5, $6) 649 - "#, 650 - &token, 651 - request_uri, 652 - SsoProviderType::Github as SsoProviderType, 653 - &provider_user_id, 654 - Some("pendinguser"), 655 - Some("pending@github.com"), 656 - ) 657 - .execute(pool) 658 - .await 659 - .unwrap(); 474 + repos 475 + .sso 476 + .create_pending_registration( 477 + &token, 478 + request_uri, 479 + SsoProviderType::Github, 480 + &provider_user_id, 481 + Some("pendinguser"), 482 + Some("pending@github.com"), 483 + false, 484 + ) 485 + .await 486 + .unwrap(); 660 487 661 - let found = sqlx::query!( 662 - r#" 663 - SELECT token, request_uri, provider as "provider: SsoProviderType", provider_user_id, 664 - provider_username, provider_email 665 - FROM sso_pending_registration 666 - WHERE token = $1 AND expires_at > NOW() 667 - "#, 668 - &token, 669 - ) 670 - .fetch_optional(pool) 671 - .await 672 - .unwrap(); 488 + let found = repos.sso.get_pending_registration(&token).await.unwrap(); 673 489 674 490 assert!(found.is_some()); 675 491 let found = found.unwrap(); 676 492 assert_eq!(found.request_uri, request_uri); 677 - assert_eq!(found.provider_username, Some("pendinguser".to_string())); 678 - assert_eq!(found.provider_email, Some("pending@github.com".to_string())); 493 + assert_eq!( 494 + found.provider_username.as_ref().unwrap().as_str(), 495 + "pendinguser" 496 + ); 497 + assert_eq!( 498 + found.provider_email.as_ref().unwrap().as_str(), 499 + "pending@github.com" 500 + ); 679 501 680 - let consumed = sqlx::query!( 681 - r#" 682 - DELETE FROM sso_pending_registration 683 - WHERE token = $1 AND expires_at > NOW() 684 - RETURNING token, request_uri 685 - "#, 686 - &token, 687 - ) 688 - .fetch_optional(pool) 689 - .await 690 - .unwrap(); 502 + let consumed = repos 503 + .sso 504 + .consume_pending_registration(&token) 505 + .await 506 + .unwrap(); 691 507 692 508 assert!(consumed.is_some()); 693 509 694 - let double_consume = sqlx::query!( 695 - r#" 696 - DELETE FROM sso_pending_registration 697 - WHERE token = $1 AND expires_at > NOW() 698 - RETURNING token 699 - "#, 700 - &token, 701 - ) 702 - .fetch_optional(pool) 703 - .await 704 - .unwrap(); 510 + let double_consume = repos 511 + .sso 512 + .consume_pending_registration(&token) 513 + .await 514 + .unwrap(); 705 515 706 516 assert!(double_consume.is_none()); 707 517 } ··· 709 519 #[tokio::test] 710 520 async fn test_sso_pending_registration_expiration() { 711 521 let _url = base_url().await; 712 - let pool = get_test_db_pool().await; 522 + let repos = get_test_repos().await; 713 523 714 - let token = format!("expired_pending_{}", uuid::Uuid::new_v4().simple()); 524 + let consumed = repos 525 + .sso 526 + .get_pending_registration("nonexistent_pending_token") 527 + .await 528 + .unwrap(); 715 529 716 - sqlx::query!( 717 - r#" 718 - INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, expires_at) 719 - VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour') 720 - "#, 721 - &token, 722 - "urn:test:expired_pending", 723 - SsoProviderType::Github as SsoProviderType, 724 - "expired_provider_user", 725 - ) 726 - .execute(pool) 727 - .await 728 - .unwrap(); 530 + assert!(consumed.is_none()); 729 531 730 - let consumed = sqlx::query!( 731 - r#" 732 - SELECT token FROM sso_pending_registration 733 - WHERE token = $1 AND expires_at > NOW() 734 - "#, 735 - &token, 736 - ) 737 - .fetch_optional(pool) 738 - .await 739 - .unwrap(); 532 + let cleaned = repos 533 + .sso 534 + .cleanup_expired_pending_registrations() 535 + .await 536 + .unwrap(); 740 537 741 - assert!(consumed.is_none()); 538 + assert!(cleaned == 0 || cleaned >= 1); 742 539 } 743 540 744 541 #[tokio::test] ··· 763 560 764 561 #[tokio::test] 765 562 async fn test_sso_complete_registration_expired_token() { 766 - let _url = base_url().await; 767 - let pool = get_test_db_pool().await; 768 - 769 - let token = format!("expired_reg_token_{}", uuid::Uuid::new_v4().simple()); 770 - 771 - sqlx::query!( 772 - r#" 773 - INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, expires_at) 774 - VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour') 775 - "#, 776 - &token, 777 - "urn:test:expired_registration", 778 - SsoProviderType::Github as SsoProviderType, 779 - "expired_user_123", 780 - ) 781 - .execute(pool) 782 - .await 783 - .unwrap(); 563 + let url = base_url().await; 564 + let client = client(); 784 565 785 - let client = client(); 786 566 let res = client 787 - .post(format!("{}/oauth/sso/complete-registration", _url)) 567 + .post(format!("{}/oauth/sso/complete-registration", url)) 788 568 .json(&json!({ 789 - "token": token, 569 + "token": format!("expired_reg_token_{}", uuid::Uuid::new_v4().simple()), 790 570 "handle": "newuser" 791 571 })) 792 572 .send() ··· 837 617 assert_eq!(body["error"], "InvalidRequest"); 838 618 } 839 619 620 + fn test_request_data() -> RequestData { 621 + RequestData { 622 + client_id: "https://test.example.com".to_string(), 623 + client_auth: None, 624 + parameters: AuthorizationRequestParameters { 625 + response_type: ResponseType::Code, 626 + client_id: "https://test.example.com".to_string(), 627 + redirect_uri: "https://test.example.com/callback".to_string(), 628 + scope: Some("atproto".to_string()), 629 + state: Some("teststate".to_string()), 630 + code_challenge: "testchallenge".to_string(), 631 + code_challenge_method: CodeChallengeMethod::S256, 632 + response_mode: None, 633 + login_hint: None, 634 + dpop_jkt: None, 635 + prompt: None, 636 + extra: None, 637 + }, 638 + expires_at: chrono::Utc::now() + chrono::Duration::hours(1), 639 + did: None, 640 + device_id: None, 641 + code: None, 642 + controller_did: None, 643 + } 644 + } 645 + 840 646 #[tokio::test] 841 647 async fn test_sso_complete_registration_success() { 842 648 let url = base_url().await; 843 - let pool = get_test_db_pool().await; 649 + let repos = get_test_repos().await; 844 650 let client = client(); 845 651 846 652 let token = format!("success_reg_token_{}", uuid::Uuid::new_v4().simple()); ··· 849 655 let provider_email = format!("sso_{}@example.com", uuid::Uuid::new_v4().simple()); 850 656 851 657 let request_uri = format!("urn:ietf:params:oauth:request_uri:{}", uuid::Uuid::new_v4()); 658 + let request_id = RequestId::new(&request_uri); 852 659 853 - sqlx::query!( 854 - r#" 855 - INSERT INTO oauth_authorization_request (id, client_id, parameters, expires_at) 856 - VALUES ($1, 'https://test.example.com', $2, NOW() + INTERVAL '1 hour') 857 - "#, 858 - &request_uri, 859 - serde_json::json!({ 860 - "redirect_uri": "https://test.example.com/callback", 861 - "scope": "atproto", 862 - "state": "teststate", 863 - "code_challenge": "testchallenge", 864 - "code_challenge_method": "S256" 865 - }), 866 - ) 867 - .execute(pool) 868 - .await 869 - .unwrap(); 660 + repos 661 + .oauth 662 + .create_authorization_request(&request_id, &test_request_data()) 663 + .await 664 + .unwrap(); 870 665 871 - sqlx::query!( 872 - r#" 873 - INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email, provider_email_verified) 874 - VALUES ($1, $2, $3, $4, $5, $6, $7) 875 - "#, 876 - &token, 877 - &request_uri, 878 - SsoProviderType::Github as SsoProviderType, 879 - &provider_user_id, 880 - Some("ssouser"), 881 - Some(&provider_email), 882 - true, 883 - ) 884 - .execute(pool) 885 - .await 886 - .unwrap(); 666 + repos 667 + .sso 668 + .create_pending_registration( 669 + &token, 670 + &request_uri, 671 + SsoProviderType::Github, 672 + &provider_user_id, 673 + Some("ssouser"), 674 + Some(&provider_email), 675 + true, 676 + ) 677 + .await 678 + .unwrap(); 887 679 888 680 let res = client 889 681 .post(format!("{}/oauth/sso/complete-registration", url)) ··· 925 717 redirect_url 926 718 ); 927 719 928 - let pending_consumed = sqlx::query!( 929 - r#"SELECT token FROM sso_pending_registration WHERE token = $1"#, 930 - &token, 931 - ) 932 - .fetch_optional(pool) 933 - .await 934 - .unwrap(); 720 + let pending_consumed = repos.sso.get_pending_registration(&token).await.unwrap(); 935 721 936 722 assert!( 937 723 pending_consumed.is_none(), 938 724 "Pending registration should be consumed after successful registration" 939 725 ); 940 726 941 - let user_exists = sqlx::query!( 942 - r#"SELECT did, email_verified FROM users WHERE did = $1"#, 943 - did_str, 944 - ) 945 - .fetch_optional(pool) 946 - .await 947 - .unwrap(); 948 - 949 - assert!(user_exists.is_some(), "User should exist in database"); 950 - let user = user_exists.unwrap(); 951 - assert!( 952 - user.email_verified, 953 - "Email should be auto-verified when provider verified it" 954 - ); 955 - 956 - let external_identity = sqlx::query!( 957 - r#" 958 - SELECT provider_user_id, provider_email_verified 959 - FROM external_identities 960 - WHERE did = $1 AND provider = $2 961 - "#, 962 - did_str, 963 - SsoProviderType::Github as SsoProviderType, 964 - ) 965 - .fetch_optional(pool) 966 - .await 967 - .unwrap(); 727 + let did: Did = did_str.parse().expect("valid DID from response"); 728 + let external_identities = repos 729 + .sso 730 + .get_external_identities_by_did(&did) 731 + .await 732 + .unwrap(); 968 733 969 734 assert!( 970 - external_identity.is_some(), 735 + !external_identities.is_empty(), 971 736 "External identity should be created" 972 737 ); 973 - let ext_id = external_identity.unwrap(); 974 - assert_eq!(ext_id.provider_user_id, provider_user_id); 975 - assert!(ext_id.provider_email_verified); 738 + let ext_id = &external_identities[0]; 739 + assert_eq!(ext_id.provider_user_id.as_str(), provider_user_id); 976 740 } 977 741 978 742 #[tokio::test] 979 743 async fn test_sso_complete_registration_multichannel_discord() { 980 744 let url = base_url().await; 981 - let pool = get_test_db_pool().await; 745 + let repos = get_test_repos().await; 982 746 let client = client(); 983 747 984 748 let token = format!("discord_reg_token_{}", uuid::Uuid::new_v4().simple()); ··· 990 754 let discord_id = "123456789012345678"; 991 755 992 756 let request_uri = format!("urn:ietf:params:oauth:request_uri:{}", uuid::Uuid::new_v4()); 757 + let request_id = RequestId::new(&request_uri); 993 758 994 - sqlx::query!( 995 - r#" 996 - INSERT INTO oauth_authorization_request (id, client_id, parameters, expires_at) 997 - VALUES ($1, 'https://test.example.com', $2, NOW() + INTERVAL '1 hour') 998 - "#, 999 - &request_uri, 1000 - serde_json::json!({ 1001 - "redirect_uri": "https://test.example.com/callback", 1002 - "scope": "atproto", 1003 - "state": "teststate", 1004 - "code_challenge": "testchallenge", 1005 - "code_challenge_method": "S256" 1006 - }), 1007 - ) 1008 - .execute(pool) 1009 - .await 1010 - .unwrap(); 759 + repos 760 + .oauth 761 + .create_authorization_request(&request_id, &test_request_data()) 762 + .await 763 + .unwrap(); 1011 764 1012 - sqlx::query!( 1013 - r#" 1014 - INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email_verified) 1015 - VALUES ($1, $2, $3, $4, $5, $6) 1016 - "#, 1017 - &token, 1018 - &request_uri, 1019 - SsoProviderType::Discord as SsoProviderType, 1020 - &provider_user_id, 1021 - Some("discorduser"), 1022 - false, 1023 - ) 1024 - .execute(pool) 1025 - .await 1026 - .unwrap(); 765 + repos 766 + .sso 767 + .create_pending_registration( 768 + &token, 769 + &request_uri, 770 + SsoProviderType::Discord, 771 + &provider_user_id, 772 + Some("discorduser"), 773 + None, 774 + false, 775 + ) 776 + .await 777 + .unwrap(); 1027 778 1028 779 let res = client 1029 780 .post(format!("{}/oauth/sso/complete-registration", url)) ··· 1049 800 ); 1050 801 1051 802 let did_str = body["did"].as_str().unwrap(); 1052 - let user = sqlx::query!( 1053 - r#"SELECT preferred_comms_channel as "preferred_comms_channel: String", discord_username FROM users WHERE did = $1"#, 1054 - did_str, 1055 - ) 1056 - .fetch_one(pool) 1057 - .await 1058 - .unwrap(); 1059 - 1060 - assert_eq!(user.preferred_comms_channel, "discord"); 1061 - assert_eq!(user.discord_username, Some(discord_id.to_string())); 803 + let did: Did = did_str.parse().expect("valid DID from response"); 804 + let user = repos 805 + .user 806 + .get_resend_verification_by_did(&did) 807 + .await 808 + .unwrap(); 809 + assert!(user.is_some(), "User should exist"); 810 + let user = user.unwrap(); 811 + assert_eq!(user.channel, CommsChannel::Discord); 812 + assert_eq!(user.discord_username.as_deref(), Some(discord_id)); 1062 813 } 1063 814 1064 815 #[tokio::test] ··· 1105 856 #[tokio::test] 1106 857 async fn test_sso_complete_registration_missing_channel_data() { 1107 858 let url = base_url().await; 1108 - let pool = get_test_db_pool().await; 859 + let repos = get_test_repos().await; 1109 860 let client = client(); 1110 861 1111 862 let token = format!("missing_channel_{}", uuid::Uuid::new_v4().simple()); 1112 863 let handle_prefix = format!("missch{}", &uuid::Uuid::new_v4().simple().to_string()[..6]); 1113 864 1114 865 let request_uri = format!("urn:ietf:params:oauth:request_uri:{}", uuid::Uuid::new_v4()); 866 + let request_id = RequestId::new(&request_uri); 1115 867 1116 - sqlx::query!( 1117 - r#" 1118 - INSERT INTO oauth_authorization_request (id, client_id, parameters, expires_at) 1119 - VALUES ($1, 'https://test.example.com', $2, NOW() + INTERVAL '1 hour') 1120 - "#, 1121 - &request_uri, 1122 - serde_json::json!({ 1123 - "redirect_uri": "https://test.example.com/callback", 1124 - "scope": "atproto", 1125 - "state": "teststate", 1126 - "code_challenge": "testchallenge", 1127 - "code_challenge_method": "S256" 1128 - }), 1129 - ) 1130 - .execute(pool) 1131 - .await 1132 - .unwrap(); 868 + repos 869 + .oauth 870 + .create_authorization_request(&request_id, &test_request_data()) 871 + .await 872 + .unwrap(); 1133 873 1134 - sqlx::query!( 1135 - r#" 1136 - INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_email_verified) 1137 - VALUES ($1, $2, $3, $4, $5) 1138 - "#, 1139 - &token, 1140 - &request_uri, 1141 - SsoProviderType::Github as SsoProviderType, 1142 - "missing_channel_user", 1143 - false, 1144 - ) 1145 - .execute(pool) 1146 - .await 1147 - .unwrap(); 874 + repos 875 + .sso 876 + .create_pending_registration( 877 + &token, 878 + &request_uri, 879 + SsoProviderType::Github, 880 + "missing_channel_user", 881 + None, 882 + None, 883 + false, 884 + ) 885 + .await 886 + .unwrap(); 1148 887 1149 888 let res = client 1150 889 .post(format!("{}/oauth/sso/complete-registration", url))
+1524
crates/tranquil-pds/tests/store_parity.rs
··· 1 + mod common; 2 + mod helpers; 3 + 4 + use std::sync::Arc; 5 + use tranquil_db::PostgresRepositories; 6 + use tranquil_db_traits::{Backlink, BacklinkPath, CommsChannel, CommsType}; 7 + use tranquil_types::{AtUri, CidLink, Did, Handle, Nsid, Rkey}; 8 + use uuid::Uuid; 9 + 10 + async fn create_store_repos() -> Arc<PostgresRepositories> { 11 + let temp_dir = std::env::temp_dir().join(format!("tranquil-parity-{}", uuid::Uuid::new_v4())); 12 + std::fs::create_dir_all(&temp_dir).expect("failed to create parity temp dir"); 13 + 14 + let metastore_dir = temp_dir.join("metastore"); 15 + let segments_dir = temp_dir.join("eventlog/segments"); 16 + let bs_data = temp_dir.join("blockstore/data"); 17 + let bs_index = temp_dir.join("blockstore/index"); 18 + std::fs::create_dir_all(&metastore_dir).unwrap(); 19 + std::fs::create_dir_all(&segments_dir).unwrap(); 20 + std::fs::create_dir_all(&bs_data).unwrap(); 21 + std::fs::create_dir_all(&bs_index).unwrap(); 22 + 23 + use tranquil_store::RealIO; 24 + use tranquil_store::blockstore::{BlockStoreConfig, TranquilBlockStore}; 25 + use tranquil_store::eventlog::{EventLog, EventLogBridge, EventLogConfig}; 26 + use tranquil_store::metastore::client::MetastoreClient; 27 + use tranquil_store::metastore::handler::HandlerPool; 28 + use tranquil_store::metastore::partitions::Partition; 29 + use tranquil_store::metastore::{Metastore, MetastoreConfig}; 30 + 31 + let metastore = 32 + Metastore::open(&metastore_dir, MetastoreConfig::default()).expect("metastore open"); 33 + 34 + let blockstore = TranquilBlockStore::open(BlockStoreConfig { 35 + data_dir: bs_data, 36 + index_dir: bs_index, 37 + max_file_size: tranquil_store::blockstore::DEFAULT_MAX_FILE_SIZE, 38 + group_commit: Default::default(), 39 + }) 40 + .expect("blockstore open"); 41 + 42 + let event_log = Arc::new( 43 + EventLog::open( 44 + EventLogConfig { 45 + segments_dir, 46 + ..EventLogConfig::default() 47 + }, 48 + RealIO::new(), 49 + ) 50 + .expect("eventlog open"), 51 + ); 52 + 53 + let bridge = Arc::new(EventLogBridge::new(Arc::clone(&event_log))); 54 + let indexes = metastore.partition(Partition::Indexes).clone(); 55 + let event_ops = metastore.event_ops(Arc::clone(&bridge)); 56 + event_ops 57 + .recover_metastore_mutations(&indexes) 58 + .expect("metastore mutation recovery failed"); 59 + 60 + let notifier = bridge.notifier(); 61 + 62 + let pool = Arc::new(HandlerPool::spawn::<RealIO>( 63 + metastore, 64 + bridge, 65 + Some(blockstore), 66 + Some(2), 67 + )); 68 + 69 + let client = MetastoreClient::<RealIO>::new(pool); 70 + 71 + Arc::new(PostgresRepositories { 72 + pool: None, 73 + repo: Arc::new(client.clone()), 74 + backlink: Arc::new(client.clone()), 75 + blob: Arc::new(client.clone()), 76 + user: Arc::new(client.clone()), 77 + session: Arc::new(client.clone()), 78 + oauth: Arc::new(client.clone()), 79 + infra: Arc::new(client.clone()), 80 + delegation: Arc::new(client.clone()), 81 + sso: Arc::new(client), 82 + event_notifier: Arc::new(notifier), 83 + }) 84 + } 85 + 86 + async fn create_pg_repos() -> Arc<PostgresRepositories> { 87 + let db_url = common::get_db_connection_string().await; 88 + let pool = sqlx::postgres::PgPoolOptions::new() 89 + .max_connections(5) 90 + .connect(&db_url) 91 + .await 92 + .expect("failed to connect for parity test"); 93 + Arc::new(PostgresRepositories::new(pool)) 94 + } 95 + 96 + struct ParityFixture { 97 + pg: Arc<PostgresRepositories>, 98 + store: Arc<PostgresRepositories>, 99 + } 100 + 101 + impl ParityFixture { 102 + async fn new() -> Self { 103 + Self { 104 + pg: create_pg_repos().await, 105 + store: create_store_repos().await, 106 + } 107 + } 108 + } 109 + 110 + fn test_did(suffix: &str) -> Did { 111 + Did::new(format!("did:plc:parity{suffix}")).unwrap() 112 + } 113 + 114 + fn test_handle(suffix: &str) -> Handle { 115 + Handle::new(format!("parity-{suffix}.test")).unwrap() 116 + } 117 + 118 + fn test_cid(seed: u8) -> CidLink { 119 + CidLink::from(helpers::make_cid(&[seed])) 120 + } 121 + 122 + fn test_nsid(name: &str) -> Nsid { 123 + Nsid::new(format!("app.bsky.feed.{name}")).unwrap() 124 + } 125 + 126 + fn test_rkey(s: &str) -> Rkey { 127 + Rkey::new(s).unwrap() 128 + } 129 + 130 + fn test_at_uri(did: &Did, collection: &Nsid, rkey: &Rkey) -> AtUri { 131 + AtUri::new(format!( 132 + "at://{}/{}/{}", 133 + did.as_str(), 134 + collection.as_str(), 135 + rkey.as_str() 136 + )) 137 + .unwrap() 138 + } 139 + 140 + async fn seed_repo( 141 + repos: &PostgresRepositories, 142 + did: &Did, 143 + handle: &Handle, 144 + root_cid: &CidLink, 145 + user_id: Uuid, 146 + ) { 147 + repos 148 + .repo 149 + .create_repo(user_id, did, handle, root_cid, "rev0") 150 + .await 151 + .unwrap(); 152 + } 153 + 154 + async fn seed_records( 155 + repos: &PostgresRepositories, 156 + repo_id: Uuid, 157 + collection: &Nsid, 158 + records: &[(Rkey, CidLink)], 159 + ) { 160 + let collections: Vec<Nsid> = records.iter().map(|_| collection.clone()).collect(); 161 + let rkeys: Vec<Rkey> = records.iter().map(|(r, _)| r.clone()).collect(); 162 + let cids: Vec<CidLink> = records.iter().map(|(_, c)| c.clone()).collect(); 163 + repos 164 + .repo 165 + .upsert_records(repo_id, &collections, &rkeys, &cids, "rev1") 166 + .await 167 + .unwrap(); 168 + } 169 + 170 + #[tokio::test] 171 + async fn parity_server_config() { 172 + let f = ParityFixture::new().await; 173 + 174 + f.pg.infra 175 + .upsert_server_config("parity_key", "parity_value") 176 + .await 177 + .unwrap(); 178 + f.store 179 + .infra 180 + .upsert_server_config("parity_key", "parity_value") 181 + .await 182 + .unwrap(); 183 + 184 + let pg_val = f.pg.infra.get_server_config("parity_key").await.unwrap(); 185 + let store_val = f.store.infra.get_server_config("parity_key").await.unwrap(); 186 + assert_eq!(pg_val, store_val); 187 + 188 + f.pg.infra.delete_server_config("parity_key").await.unwrap(); 189 + f.store 190 + .infra 191 + .delete_server_config("parity_key") 192 + .await 193 + .unwrap(); 194 + 195 + let pg_gone = f.pg.infra.get_server_config("parity_key").await.unwrap(); 196 + let store_gone = f.store.infra.get_server_config("parity_key").await.unwrap(); 197 + assert_eq!(pg_gone, None); 198 + assert_eq!(store_gone, None); 199 + } 200 + 201 + #[tokio::test] 202 + async fn parity_health_check() { 203 + let f = ParityFixture::new().await; 204 + 205 + let pg_health = f.pg.infra.health_check().await.unwrap(); 206 + let store_health = f.store.infra.health_check().await.unwrap(); 207 + assert!(pg_health); 208 + assert!(store_health); 209 + } 210 + 211 + #[tokio::test] 212 + async fn parity_rkey_sort_order() { 213 + let f = ParityFixture::new().await; 214 + let uid = Uuid::new_v4(); 215 + let did = test_did("rkey"); 216 + let handle = test_handle("rkey"); 217 + let root_cid = test_cid(0); 218 + let collection = test_nsid("post"); 219 + 220 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 221 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 222 + 223 + let records: Vec<(Rkey, CidLink)> = (0u8..10) 224 + .map(|i| { 225 + let rkey = test_rkey(&format!("3l{i}aaaaaaaa{i}")); 226 + let cid = test_cid(i + 1); 227 + (rkey, cid) 228 + }) 229 + .collect(); 230 + 231 + seed_records(&f.pg, uid, &collection, &records).await; 232 + seed_records(&f.store, uid, &collection, &records).await; 233 + 234 + let pg_fwd = 235 + f.pg.repo 236 + .list_records(uid, &collection, None, 100, false, None, None) 237 + .await 238 + .unwrap(); 239 + let store_fwd = f 240 + .store 241 + .repo 242 + .list_records(uid, &collection, None, 100, false, None, None) 243 + .await 244 + .unwrap(); 245 + 246 + let pg_rkeys: Vec<&str> = pg_fwd.iter().map(|r| r.rkey.as_str()).collect(); 247 + let store_rkeys: Vec<&str> = store_fwd.iter().map(|r| r.rkey.as_str()).collect(); 248 + assert_eq!(pg_rkeys, store_rkeys, "forward rkey order mismatch"); 249 + 250 + let pg_rev = 251 + f.pg.repo 252 + .list_records(uid, &collection, None, 100, true, None, None) 253 + .await 254 + .unwrap(); 255 + let store_rev = f 256 + .store 257 + .repo 258 + .list_records(uid, &collection, None, 100, true, None, None) 259 + .await 260 + .unwrap(); 261 + 262 + let pg_rkeys_rev: Vec<&str> = pg_rev.iter().map(|r| r.rkey.as_str()).collect(); 263 + let store_rkeys_rev: Vec<&str> = store_rev.iter().map(|r| r.rkey.as_str()).collect(); 264 + assert_eq!(pg_rkeys_rev, store_rkeys_rev, "reverse rkey order mismatch"); 265 + 266 + let pg_cids: Vec<&str> = pg_fwd.iter().map(|r| r.record_cid.as_str()).collect(); 267 + let store_cids: Vec<&str> = store_fwd.iter().map(|r| r.record_cid.as_str()).collect(); 268 + assert_eq!(pg_cids, store_cids, "cid mapping mismatch"); 269 + } 270 + 271 + #[tokio::test] 272 + async fn parity_cursor_pagination() { 273 + let f = ParityFixture::new().await; 274 + let uid = Uuid::new_v4(); 275 + let did = test_did("cursor"); 276 + let handle = test_handle("cursor"); 277 + let root_cid = test_cid(0); 278 + let collection = test_nsid("post"); 279 + 280 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 281 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 282 + 283 + let records: Vec<(Rkey, CidLink)> = (0u8..20) 284 + .map(|i| { 285 + let rkey = test_rkey(&format!("3l{:02}aaaaaaaaa", i)); 286 + let cid = test_cid(i + 1); 287 + (rkey, cid) 288 + }) 289 + .collect(); 290 + 291 + seed_records(&f.pg, uid, &collection, &records).await; 292 + seed_records(&f.store, uid, &collection, &records).await; 293 + 294 + let mut pg_all = Vec::new(); 295 + let mut store_all = Vec::new(); 296 + let mut pg_cursor: Option<Rkey> = None; 297 + let mut store_cursor: Option<Rkey> = None; 298 + let limit = 5i64; 299 + let mut pages = 0; 300 + 301 + loop { 302 + let pg_page = 303 + f.pg.repo 304 + .list_records( 305 + uid, 306 + &collection, 307 + pg_cursor.as_ref(), 308 + limit, 309 + false, 310 + None, 311 + None, 312 + ) 313 + .await 314 + .unwrap(); 315 + let store_page = f 316 + .store 317 + .repo 318 + .list_records( 319 + uid, 320 + &collection, 321 + store_cursor.as_ref(), 322 + limit, 323 + false, 324 + None, 325 + None, 326 + ) 327 + .await 328 + .unwrap(); 329 + 330 + assert_eq!( 331 + pg_page.len(), 332 + store_page.len(), 333 + "page size mismatch at page {pages}" 334 + ); 335 + 336 + let pg_rkeys: Vec<&str> = pg_page.iter().map(|r| r.rkey.as_str()).collect(); 337 + let store_rkeys: Vec<&str> = store_page.iter().map(|r| r.rkey.as_str()).collect(); 338 + assert_eq!( 339 + pg_rkeys, store_rkeys, 340 + "page content mismatch at page {pages}" 341 + ); 342 + 343 + pg_all.extend(pg_page.iter().map(|r| r.rkey.clone())); 344 + store_all.extend(store_page.iter().map(|r| r.rkey.clone())); 345 + 346 + if pg_page.len() < limit as usize { 347 + break; 348 + } 349 + 350 + pg_cursor = pg_page.last().map(|r| r.rkey.clone()); 351 + store_cursor = store_page.last().map(|r| r.rkey.clone()); 352 + pages += 1; 353 + } 354 + 355 + assert_eq!(pg_all.len(), 20); 356 + assert_eq!(store_all.len(), 20); 357 + } 358 + 359 + #[tokio::test] 360 + async fn parity_cursor_pagination_reverse() { 361 + let f = ParityFixture::new().await; 362 + let uid = Uuid::new_v4(); 363 + let did = test_did("currev"); 364 + let handle = test_handle("currev"); 365 + let root_cid = test_cid(0); 366 + let collection = test_nsid("post"); 367 + 368 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 369 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 370 + 371 + let records: Vec<(Rkey, CidLink)> = (0u8..15) 372 + .map(|i| { 373 + let rkey = test_rkey(&format!("3l{:02}aaaaaaaaa", i)); 374 + let cid = test_cid(i + 1); 375 + (rkey, cid) 376 + }) 377 + .collect(); 378 + 379 + seed_records(&f.pg, uid, &collection, &records).await; 380 + seed_records(&f.store, uid, &collection, &records).await; 381 + 382 + let mut pg_all = Vec::new(); 383 + let mut store_all = Vec::new(); 384 + let mut pg_cursor: Option<Rkey> = None; 385 + let mut store_cursor: Option<Rkey> = None; 386 + let limit = 4i64; 387 + 388 + loop { 389 + let pg_page = 390 + f.pg.repo 391 + .list_records( 392 + uid, 393 + &collection, 394 + pg_cursor.as_ref(), 395 + limit, 396 + true, 397 + None, 398 + None, 399 + ) 400 + .await 401 + .unwrap(); 402 + let store_page = f 403 + .store 404 + .repo 405 + .list_records( 406 + uid, 407 + &collection, 408 + store_cursor.as_ref(), 409 + limit, 410 + true, 411 + None, 412 + None, 413 + ) 414 + .await 415 + .unwrap(); 416 + 417 + let pg_rkeys: Vec<&str> = pg_page.iter().map(|r| r.rkey.as_str()).collect(); 418 + let store_rkeys: Vec<&str> = store_page.iter().map(|r| r.rkey.as_str()).collect(); 419 + assert_eq!(pg_rkeys, store_rkeys, "reverse page mismatch"); 420 + 421 + pg_all.extend(pg_page.iter().map(|r| r.rkey.clone())); 422 + store_all.extend(store_page.iter().map(|r| r.rkey.clone())); 423 + 424 + if pg_page.len() < limit as usize { 425 + break; 426 + } 427 + 428 + pg_cursor = pg_page.last().map(|r| r.rkey.clone()); 429 + store_cursor = store_page.last().map(|r| r.rkey.clone()); 430 + } 431 + 432 + assert_eq!(pg_all.len(), 15); 433 + assert_eq!(store_all.len(), 15); 434 + } 435 + 436 + #[tokio::test] 437 + async fn parity_rkey_range_query() { 438 + let f = ParityFixture::new().await; 439 + let uid = Uuid::new_v4(); 440 + let did = test_did("range"); 441 + let handle = test_handle("range"); 442 + let root_cid = test_cid(0); 443 + let collection = test_nsid("post"); 444 + 445 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 446 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 447 + 448 + let records: Vec<(Rkey, CidLink)> = (0u8..10) 449 + .map(|i| { 450 + let rkey = test_rkey(&format!("3l{:02}aaaaaaaaa", i)); 451 + let cid = test_cid(i + 1); 452 + (rkey, cid) 453 + }) 454 + .collect(); 455 + 456 + seed_records(&f.pg, uid, &collection, &records).await; 457 + seed_records(&f.store, uid, &collection, &records).await; 458 + 459 + let start = test_rkey("3l03aaaaaaaaa"); 460 + let end = test_rkey("3l07aaaaaaaaa"); 461 + 462 + let pg_range = 463 + f.pg.repo 464 + .list_records(uid, &collection, None, 100, false, Some(&start), Some(&end)) 465 + .await 466 + .unwrap(); 467 + let store_range = f 468 + .store 469 + .repo 470 + .list_records(uid, &collection, None, 100, false, Some(&start), Some(&end)) 471 + .await 472 + .unwrap(); 473 + 474 + let pg_rkeys: Vec<&str> = pg_range.iter().map(|r| r.rkey.as_str()).collect(); 475 + let store_rkeys: Vec<&str> = store_range.iter().map(|r| r.rkey.as_str()).collect(); 476 + assert_eq!(pg_rkeys, store_rkeys, "range query mismatch"); 477 + } 478 + 479 + #[tokio::test] 480 + async fn parity_collection_listing() { 481 + let f = ParityFixture::new().await; 482 + let uid = Uuid::new_v4(); 483 + let did = test_did("colls"); 484 + let handle = test_handle("colls"); 485 + let root_cid = test_cid(0); 486 + 487 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 488 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 489 + 490 + let post_ns = test_nsid("post"); 491 + let like_ns = Nsid::new("app.bsky.feed.like").unwrap(); 492 + let repost_ns = Nsid::new("app.bsky.feed.repost").unwrap(); 493 + let follow_ns = Nsid::new("app.bsky.graph.follow").unwrap(); 494 + 495 + let post_records = vec![(test_rkey("3laaaaaaaaa01"), test_cid(1))]; 496 + let like_records = vec![(test_rkey("3laaaaaaaaa02"), test_cid(2))]; 497 + let repost_records = vec![(test_rkey("3laaaaaaaaa03"), test_cid(3))]; 498 + let follow_records = vec![(test_rkey("3laaaaaaaaa04"), test_cid(4))]; 499 + 500 + seed_records(&f.pg, uid, &post_ns, &post_records).await; 501 + seed_records(&f.pg, uid, &like_ns, &like_records).await; 502 + seed_records(&f.pg, uid, &repost_ns, &repost_records).await; 503 + seed_records(&f.pg, uid, &follow_ns, &follow_records).await; 504 + 505 + seed_records(&f.store, uid, &post_ns, &post_records).await; 506 + seed_records(&f.store, uid, &like_ns, &like_records).await; 507 + seed_records(&f.store, uid, &repost_ns, &repost_records).await; 508 + seed_records(&f.store, uid, &follow_ns, &follow_records).await; 509 + 510 + let mut pg_colls: Vec<String> = 511 + f.pg.repo 512 + .list_collections(uid) 513 + .await 514 + .unwrap() 515 + .into_iter() 516 + .map(|n| n.as_str().to_owned()) 517 + .collect(); 518 + pg_colls.sort(); 519 + 520 + let mut store_colls: Vec<String> = f 521 + .store 522 + .repo 523 + .list_collections(uid) 524 + .await 525 + .unwrap() 526 + .into_iter() 527 + .map(|n| n.as_str().to_owned()) 528 + .collect(); 529 + store_colls.sort(); 530 + 531 + assert_eq!(pg_colls, store_colls, "collection listing mismatch"); 532 + assert_eq!(pg_colls.len(), 4); 533 + 534 + let pg_count = f.pg.repo.count_records(uid).await.unwrap(); 535 + let store_count = f.store.repo.count_records(uid).await.unwrap(); 536 + assert_eq!(pg_count, store_count, "record count mismatch"); 537 + assert_eq!(pg_count, 4); 538 + } 539 + 540 + #[tokio::test] 541 + async fn parity_record_get_and_delete() { 542 + let f = ParityFixture::new().await; 543 + let uid = Uuid::new_v4(); 544 + let did = test_did("getdel"); 545 + let handle = test_handle("getdel"); 546 + let root_cid = test_cid(0); 547 + let collection = test_nsid("post"); 548 + let rkey = test_rkey("3laaaaaaaaa01"); 549 + let cid = test_cid(1); 550 + 551 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 552 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 553 + 554 + seed_records(&f.pg, uid, &collection, &[(rkey.clone(), cid.clone())]).await; 555 + seed_records(&f.store, uid, &collection, &[(rkey.clone(), cid.clone())]).await; 556 + 557 + let pg_cid = 558 + f.pg.repo 559 + .get_record_cid(uid, &collection, &rkey) 560 + .await 561 + .unwrap(); 562 + let store_cid = f 563 + .store 564 + .repo 565 + .get_record_cid(uid, &collection, &rkey) 566 + .await 567 + .unwrap(); 568 + assert_eq!(pg_cid, store_cid, "get_record_cid mismatch"); 569 + assert!(pg_cid.is_some()); 570 + 571 + f.pg.repo 572 + .delete_records(uid, &[collection.clone()], &[rkey.clone()]) 573 + .await 574 + .unwrap(); 575 + f.store 576 + .repo 577 + .delete_records(uid, &[collection.clone()], &[rkey.clone()]) 578 + .await 579 + .unwrap(); 580 + 581 + let pg_gone = 582 + f.pg.repo 583 + .get_record_cid(uid, &collection, &rkey) 584 + .await 585 + .unwrap(); 586 + let store_gone = f 587 + .store 588 + .repo 589 + .get_record_cid(uid, &collection, &rkey) 590 + .await 591 + .unwrap(); 592 + assert_eq!(pg_gone, None); 593 + assert_eq!(store_gone, None); 594 + } 595 + 596 + #[tokio::test] 597 + async fn parity_backlink_queries() { 598 + let f = ParityFixture::new().await; 599 + let uid = Uuid::new_v4(); 600 + let did = test_did("blink"); 601 + let handle = test_handle("blink"); 602 + let root_cid = test_cid(0); 603 + let like_ns = Nsid::new("app.bsky.feed.like").unwrap(); 604 + let target_did = test_did("target"); 605 + 606 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 607 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 608 + 609 + let rkey1 = test_rkey("3laaaaaaaaa01"); 610 + let rkey2 = test_rkey("3laaaaaaaaa02"); 611 + let uri1 = test_at_uri(&did, &like_ns, &rkey1); 612 + let uri2 = test_at_uri(&did, &like_ns, &rkey2); 613 + let target_uri = format!( 614 + "at://{}/app.bsky.feed.post/3laaaaaaaaa99", 615 + target_did.as_str() 616 + ); 617 + 618 + let backlinks = vec![ 619 + Backlink { 620 + uri: uri1.clone(), 621 + path: BacklinkPath::Subject, 622 + link_to: target_uri.clone(), 623 + }, 624 + Backlink { 625 + uri: uri2.clone(), 626 + path: BacklinkPath::Subject, 627 + link_to: target_uri.clone(), 628 + }, 629 + ]; 630 + 631 + f.pg.backlink.add_backlinks(uid, &backlinks).await.unwrap(); 632 + f.store 633 + .backlink 634 + .add_backlinks(uid, &backlinks) 635 + .await 636 + .unwrap(); 637 + 638 + let conflict_backlink = Backlink { 639 + uri: uri1.clone(), 640 + path: BacklinkPath::Subject, 641 + link_to: target_uri.clone(), 642 + }; 643 + 644 + let pg_conflicts = 645 + f.pg.backlink 646 + .get_backlink_conflicts(uid, &like_ns, &[conflict_backlink.clone()]) 647 + .await 648 + .unwrap(); 649 + let store_conflicts = f 650 + .store 651 + .backlink 652 + .get_backlink_conflicts(uid, &like_ns, &[conflict_backlink]) 653 + .await 654 + .unwrap(); 655 + 656 + assert_eq!( 657 + pg_conflicts.len(), 658 + store_conflicts.len(), 659 + "backlink conflict count mismatch" 660 + ); 661 + 662 + f.pg.backlink.remove_backlinks_by_uri(&uri1).await.unwrap(); 663 + f.store 664 + .backlink 665 + .remove_backlinks_by_uri(&uri1) 666 + .await 667 + .unwrap(); 668 + 669 + let post_removal = Backlink { 670 + uri: uri1.clone(), 671 + path: BacklinkPath::Subject, 672 + link_to: target_uri.clone(), 673 + }; 674 + 675 + let pg_after = 676 + f.pg.backlink 677 + .get_backlink_conflicts(uid, &like_ns, &[post_removal.clone()]) 678 + .await 679 + .unwrap(); 680 + let store_after = f 681 + .store 682 + .backlink 683 + .get_backlink_conflicts(uid, &like_ns, &[post_removal]) 684 + .await 685 + .unwrap(); 686 + 687 + assert_eq!( 688 + pg_after.len(), 689 + store_after.len(), 690 + "backlink conflicts after removal mismatch" 691 + ); 692 + } 693 + 694 + #[tokio::test] 695 + async fn parity_backlink_remove_by_repo() { 696 + let f = ParityFixture::new().await; 697 + let uid = Uuid::new_v4(); 698 + let did = test_did("blrep"); 699 + let handle = test_handle("blrep"); 700 + let root_cid = test_cid(0); 701 + let like_ns = Nsid::new("app.bsky.feed.like").unwrap(); 702 + 703 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 704 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 705 + 706 + let rkey = test_rkey("3laaaaaaaaa01"); 707 + let uri = test_at_uri(&did, &like_ns, &rkey); 708 + let backlinks = vec![Backlink { 709 + uri: uri.clone(), 710 + path: BacklinkPath::Subject, 711 + link_to: "at://did:plc:sometarget/app.bsky.feed.post/abc".to_owned(), 712 + }]; 713 + 714 + f.pg.backlink.add_backlinks(uid, &backlinks).await.unwrap(); 715 + f.store 716 + .backlink 717 + .add_backlinks(uid, &backlinks) 718 + .await 719 + .unwrap(); 720 + 721 + f.pg.backlink.remove_backlinks_by_repo(uid).await.unwrap(); 722 + f.store 723 + .backlink 724 + .remove_backlinks_by_repo(uid) 725 + .await 726 + .unwrap(); 727 + 728 + let probe = Backlink { 729 + uri, 730 + path: BacklinkPath::Subject, 731 + link_to: "at://did:plc:sometarget/app.bsky.feed.post/abc".to_owned(), 732 + }; 733 + let pg_after = 734 + f.pg.backlink 735 + .get_backlink_conflicts(uid, &like_ns, &[probe.clone()]) 736 + .await 737 + .unwrap(); 738 + let store_after = f 739 + .store 740 + .backlink 741 + .get_backlink_conflicts(uid, &like_ns, &[probe]) 742 + .await 743 + .unwrap(); 744 + assert_eq!(pg_after.len(), 0); 745 + assert_eq!(store_after.len(), 0); 746 + } 747 + 748 + #[tokio::test] 749 + async fn parity_blob_metadata() { 750 + let f = ParityFixture::new().await; 751 + let uid = Uuid::new_v4(); 752 + let did = test_did("blob"); 753 + let handle = test_handle("blob"); 754 + let root_cid = test_cid(0); 755 + 756 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 757 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 758 + 759 + let blob_cid1 = test_cid(101); 760 + let blob_cid2 = test_cid(102); 761 + let blob_cid3 = test_cid(103); 762 + 763 + let blobs = [ 764 + (&blob_cid1, "image/png", 1024i64, "blobs/a.png"), 765 + (&blob_cid2, "image/jpeg", 2048, "blobs/b.jpg"), 766 + (&blob_cid3, "application/pdf", 4096, "blobs/c.pdf"), 767 + ]; 768 + 769 + blobs.iter().for_each(|(cid, mime, size, key)| { 770 + let pg = Arc::clone(&f.pg); 771 + let store = Arc::clone(&f.store); 772 + let cid = (*cid).clone(); 773 + let size = *size; 774 + let mime = mime.to_string(); 775 + let key = key.to_string(); 776 + tokio::task::block_in_place(|| { 777 + tokio::runtime::Handle::current().block_on(async { 778 + pg.blob 779 + .insert_blob(&cid, &mime, size, uid, &key) 780 + .await 781 + .unwrap(); 782 + store 783 + .blob 784 + .insert_blob(&cid, &mime, size, uid, &key) 785 + .await 786 + .unwrap(); 787 + }); 788 + }); 789 + }); 790 + 791 + let pg_meta = 792 + f.pg.blob 793 + .get_blob_metadata(&blob_cid1) 794 + .await 795 + .unwrap() 796 + .unwrap(); 797 + let store_meta = f 798 + .store 799 + .blob 800 + .get_blob_metadata(&blob_cid1) 801 + .await 802 + .unwrap() 803 + .unwrap(); 804 + assert_eq!(pg_meta.mime_type, store_meta.mime_type); 805 + assert_eq!(pg_meta.size_bytes, store_meta.size_bytes); 806 + assert_eq!(pg_meta.storage_key, store_meta.storage_key); 807 + 808 + let pg_key = f.pg.blob.get_blob_storage_key(&blob_cid2).await.unwrap(); 809 + let store_key = f.store.blob.get_blob_storage_key(&blob_cid2).await.unwrap(); 810 + assert_eq!(pg_key, store_key); 811 + 812 + let pg_count = f.pg.blob.count_blobs_by_user(uid).await.unwrap(); 813 + let store_count = f.store.blob.count_blobs_by_user(uid).await.unwrap(); 814 + assert_eq!(pg_count, store_count); 815 + assert_eq!(pg_count, 3); 816 + 817 + let pg_list = f.pg.blob.list_blobs_by_user(uid, None, 100).await.unwrap(); 818 + let store_list = f 819 + .store 820 + .blob 821 + .list_blobs_by_user(uid, None, 100) 822 + .await 823 + .unwrap(); 824 + assert_eq!(pg_list.len(), store_list.len()); 825 + } 826 + 827 + #[tokio::test] 828 + async fn parity_blob_pagination() { 829 + let f = ParityFixture::new().await; 830 + let uid = Uuid::new_v4(); 831 + let did = test_did("blobpg"); 832 + let handle = test_handle("blobpg"); 833 + let root_cid = test_cid(0); 834 + 835 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 836 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 837 + 838 + (0u8..8).for_each(|i| { 839 + let cid = test_cid(200 + i); 840 + let key = format!("blobs/pg_{i}.bin"); 841 + let pg = Arc::clone(&f.pg); 842 + let store = Arc::clone(&f.store); 843 + tokio::task::block_in_place(|| { 844 + tokio::runtime::Handle::current().block_on(async { 845 + pg.blob 846 + .insert_blob( 847 + &cid, 848 + "application/octet-stream", 849 + 512 * (i as i64 + 1), 850 + uid, 851 + &key, 852 + ) 853 + .await 854 + .unwrap(); 855 + store 856 + .blob 857 + .insert_blob( 858 + &cid, 859 + "application/octet-stream", 860 + 512 * (i as i64 + 1), 861 + uid, 862 + &key, 863 + ) 864 + .await 865 + .unwrap(); 866 + }); 867 + }); 868 + }); 869 + 870 + let mut pg_all = Vec::new(); 871 + let mut store_all = Vec::new(); 872 + let mut pg_cursor: Option<String> = None; 873 + let mut store_cursor: Option<String> = None; 874 + let limit = 3i64; 875 + 876 + loop { 877 + let pg_page = 878 + f.pg.blob 879 + .list_blobs_by_user(uid, pg_cursor.as_deref(), limit) 880 + .await 881 + .unwrap(); 882 + let store_page = f 883 + .store 884 + .blob 885 + .list_blobs_by_user(uid, store_cursor.as_deref(), limit) 886 + .await 887 + .unwrap(); 888 + 889 + assert_eq!(pg_page.len(), store_page.len(), "blob page size mismatch"); 890 + 891 + let pg_cids: Vec<&str> = pg_page.iter().map(|c| c.as_str()).collect(); 892 + let store_cids: Vec<&str> = store_page.iter().map(|c| c.as_str()).collect(); 893 + assert_eq!(pg_cids, store_cids, "blob page content mismatch"); 894 + 895 + pg_all.extend(pg_page.iter().map(|c| c.as_str().to_owned())); 896 + store_all.extend(store_page.iter().map(|c| c.as_str().to_owned())); 897 + 898 + if pg_page.len() < limit as usize { 899 + break; 900 + } 901 + 902 + pg_cursor = pg_page.last().map(|c| c.as_str().to_owned()); 903 + store_cursor = store_page.last().map(|c| c.as_str().to_owned()); 904 + } 905 + 906 + assert_eq!(pg_all.len(), 8); 907 + assert_eq!(store_all.len(), 8); 908 + } 909 + 910 + #[tokio::test] 911 + async fn parity_blob_duplicate_insert() { 912 + let f = ParityFixture::new().await; 913 + let uid = Uuid::new_v4(); 914 + let did = test_did("blobdup"); 915 + let handle = test_handle("blobdup"); 916 + let root_cid = test_cid(0); 917 + 918 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 919 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 920 + 921 + let cid = test_cid(150); 922 + 923 + let pg_first = 924 + f.pg.blob 925 + .insert_blob(&cid, "image/png", 1024, uid, "blobs/dup.png") 926 + .await 927 + .unwrap(); 928 + let store_first = f 929 + .store 930 + .blob 931 + .insert_blob(&cid, "image/png", 1024, uid, "blobs/dup.png") 932 + .await 933 + .unwrap(); 934 + assert_eq!(pg_first, store_first); 935 + 936 + let pg_dup = 937 + f.pg.blob 938 + .insert_blob(&cid, "image/png", 1024, uid, "blobs/dup.png") 939 + .await 940 + .unwrap(); 941 + let store_dup = f 942 + .store 943 + .blob 944 + .insert_blob(&cid, "image/png", 1024, uid, "blobs/dup.png") 945 + .await 946 + .unwrap(); 947 + assert_eq!(pg_dup, store_dup); 948 + } 949 + 950 + #[tokio::test] 951 + async fn parity_get_all_records() { 952 + let f = ParityFixture::new().await; 953 + let uid = Uuid::new_v4(); 954 + let did = test_did("allrec"); 955 + let handle = test_handle("allrec"); 956 + let root_cid = test_cid(0); 957 + 958 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 959 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 960 + 961 + let post_ns = test_nsid("post"); 962 + let like_ns = Nsid::new("app.bsky.feed.like").unwrap(); 963 + 964 + let posts = vec![ 965 + (test_rkey("3laaaaaaaaa01"), test_cid(1)), 966 + (test_rkey("3laaaaaaaaa02"), test_cid(2)), 967 + ]; 968 + let likes = vec![(test_rkey("3laaaaaaaaa03"), test_cid(3))]; 969 + 970 + seed_records(&f.pg, uid, &post_ns, &posts).await; 971 + seed_records(&f.pg, uid, &like_ns, &likes).await; 972 + seed_records(&f.store, uid, &post_ns, &posts).await; 973 + seed_records(&f.store, uid, &like_ns, &likes).await; 974 + 975 + let mut pg_all = f.pg.repo.get_all_records(uid).await.unwrap(); 976 + let mut store_all = f.store.repo.get_all_records(uid).await.unwrap(); 977 + 978 + pg_all.sort_by(|a, b| { 979 + a.collection 980 + .as_str() 981 + .cmp(b.collection.as_str()) 982 + .then(a.rkey.as_str().cmp(b.rkey.as_str())) 983 + }); 984 + store_all.sort_by(|a, b| { 985 + a.collection 986 + .as_str() 987 + .cmp(b.collection.as_str()) 988 + .then(a.rkey.as_str().cmp(b.rkey.as_str())) 989 + }); 990 + 991 + assert_eq!(pg_all.len(), store_all.len()); 992 + pg_all.iter().zip(store_all.iter()).for_each(|(p, s)| { 993 + assert_eq!(p.collection.as_str(), s.collection.as_str()); 994 + assert_eq!(p.rkey.as_str(), s.rkey.as_str()); 995 + assert_eq!(p.record_cid.as_str(), s.record_cid.as_str()); 996 + }); 997 + } 998 + 999 + #[tokio::test] 1000 + async fn parity_comms_queue() { 1001 + let f = ParityFixture::new().await; 1002 + let uid = Uuid::new_v4(); 1003 + 1004 + let pg_id = 1005 + f.pg.infra 1006 + .enqueue_comms( 1007 + Some(uid), 1008 + CommsChannel::Email, 1009 + CommsType::Welcome, 1010 + "test@example.com", 1011 + Some("Welcome"), 1012 + "Welcome body", 1013 + None, 1014 + ) 1015 + .await 1016 + .unwrap(); 1017 + 1018 + let store_id = f 1019 + .store 1020 + .infra 1021 + .enqueue_comms( 1022 + Some(uid), 1023 + CommsChannel::Email, 1024 + CommsType::Welcome, 1025 + "test@example.com", 1026 + Some("Welcome"), 1027 + "Welcome body", 1028 + None, 1029 + ) 1030 + .await 1031 + .unwrap(); 1032 + 1033 + assert_ne!(pg_id, Uuid::nil()); 1034 + assert_ne!(store_id, Uuid::nil()); 1035 + 1036 + let pg_latest = 1037 + f.pg.infra 1038 + .get_latest_comms_for_user(uid, CommsType::Welcome, 10) 1039 + .await 1040 + .unwrap(); 1041 + let store_latest = f 1042 + .store 1043 + .infra 1044 + .get_latest_comms_for_user(uid, CommsType::Welcome, 10) 1045 + .await 1046 + .unwrap(); 1047 + 1048 + assert_eq!(pg_latest.len(), store_latest.len()); 1049 + assert_eq!(pg_latest[0].body, store_latest[0].body); 1050 + 1051 + let pg_count = 1052 + f.pg.infra 1053 + .count_comms_by_type(uid, CommsType::Welcome) 1054 + .await 1055 + .unwrap(); 1056 + let store_count = f 1057 + .store 1058 + .infra 1059 + .count_comms_by_type(uid, CommsType::Welcome) 1060 + .await 1061 + .unwrap(); 1062 + assert_eq!(pg_count, store_count); 1063 + assert_eq!(pg_count, 1); 1064 + } 1065 + 1066 + #[tokio::test] 1067 + async fn parity_invite_codes() { 1068 + let f = ParityFixture::new().await; 1069 + let code = format!("parity-invite-{}", Uuid::new_v4()); 1070 + 1071 + let pg_created = f.pg.infra.create_invite_code(&code, 5, None).await.unwrap(); 1072 + let store_created = f 1073 + .store 1074 + .infra 1075 + .create_invite_code(&code, 5, None) 1076 + .await 1077 + .unwrap(); 1078 + assert_eq!(pg_created, store_created); 1079 + 1080 + let pg_uses = 1081 + f.pg.infra 1082 + .get_invite_code_available_uses(&code) 1083 + .await 1084 + .unwrap(); 1085 + let store_uses = f 1086 + .store 1087 + .infra 1088 + .get_invite_code_available_uses(&code) 1089 + .await 1090 + .unwrap(); 1091 + assert_eq!(pg_uses, store_uses); 1092 + assert_eq!(pg_uses, Some(5)); 1093 + } 1094 + 1095 + #[tokio::test] 1096 + async fn parity_account_preferences() { 1097 + let f = ParityFixture::new().await; 1098 + let uid = Uuid::new_v4(); 1099 + let did = test_did("prefs"); 1100 + let handle = test_handle("prefs"); 1101 + let root_cid = test_cid(0); 1102 + 1103 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 1104 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 1105 + 1106 + let pref_value = serde_json::json!({ 1107 + "$type": "app.bsky.actor.defs#adultContentPref", 1108 + "enabled": false 1109 + }); 1110 + 1111 + f.pg.infra 1112 + .upsert_account_preference( 1113 + uid, 1114 + "app.bsky.actor.defs#adultContentPref/0", 1115 + pref_value.clone(), 1116 + ) 1117 + .await 1118 + .unwrap(); 1119 + f.store 1120 + .infra 1121 + .upsert_account_preference(uid, "app.bsky.actor.defs#adultContentPref/0", pref_value) 1122 + .await 1123 + .unwrap(); 1124 + 1125 + let mut pg_prefs = f.pg.infra.get_account_preferences(uid).await.unwrap(); 1126 + let mut store_prefs = f.store.infra.get_account_preferences(uid).await.unwrap(); 1127 + 1128 + pg_prefs.sort_by(|a, b| a.0.cmp(&b.0)); 1129 + store_prefs.sort_by(|a, b| a.0.cmp(&b.0)); 1130 + 1131 + assert_eq!(pg_prefs.len(), store_prefs.len()); 1132 + pg_prefs.iter().zip(store_prefs.iter()).for_each(|(p, s)| { 1133 + assert_eq!(p.0, s.0); 1134 + assert_eq!(p.1, s.1); 1135 + }); 1136 + } 1137 + 1138 + #[tokio::test] 1139 + async fn parity_record_upsert_overwrites() { 1140 + let f = ParityFixture::new().await; 1141 + let uid = Uuid::new_v4(); 1142 + let did = test_did("upsert"); 1143 + let handle = test_handle("upsert"); 1144 + let root_cid = test_cid(0); 1145 + let collection = test_nsid("post"); 1146 + let rkey = test_rkey("3laaaaaaaaa01"); 1147 + 1148 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 1149 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 1150 + 1151 + let cid_v1 = test_cid(1); 1152 + seed_records(&f.pg, uid, &collection, &[(rkey.clone(), cid_v1.clone())]).await; 1153 + seed_records( 1154 + &f.store, 1155 + uid, 1156 + &collection, 1157 + &[(rkey.clone(), cid_v1.clone())], 1158 + ) 1159 + .await; 1160 + 1161 + let cid_v2 = test_cid(2); 1162 + seed_records(&f.pg, uid, &collection, &[(rkey.clone(), cid_v2.clone())]).await; 1163 + seed_records( 1164 + &f.store, 1165 + uid, 1166 + &collection, 1167 + &[(rkey.clone(), cid_v2.clone())], 1168 + ) 1169 + .await; 1170 + 1171 + let pg_cid = 1172 + f.pg.repo 1173 + .get_record_cid(uid, &collection, &rkey) 1174 + .await 1175 + .unwrap(); 1176 + let store_cid = f 1177 + .store 1178 + .repo 1179 + .get_record_cid(uid, &collection, &rkey) 1180 + .await 1181 + .unwrap(); 1182 + assert_eq!(pg_cid, store_cid); 1183 + assert_eq!(pg_cid.unwrap().as_str(), cid_v2.as_str()); 1184 + 1185 + let pg_count = f.pg.repo.count_records(uid).await.unwrap(); 1186 + let store_count = f.store.repo.count_records(uid).await.unwrap(); 1187 + assert_eq!(pg_count, 1); 1188 + assert_eq!(store_count, 1); 1189 + } 1190 + 1191 + #[tokio::test] 1192 + async fn parity_empty_queries() { 1193 + let f = ParityFixture::new().await; 1194 + let uid = Uuid::new_v4(); 1195 + let did = test_did("empty"); 1196 + let handle = test_handle("empty"); 1197 + let root_cid = test_cid(0); 1198 + let collection = test_nsid("post"); 1199 + 1200 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 1201 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 1202 + 1203 + let pg_records = 1204 + f.pg.repo 1205 + .list_records(uid, &collection, None, 100, false, None, None) 1206 + .await 1207 + .unwrap(); 1208 + let store_records = f 1209 + .store 1210 + .repo 1211 + .list_records(uid, &collection, None, 100, false, None, None) 1212 + .await 1213 + .unwrap(); 1214 + assert_eq!(pg_records.len(), 0); 1215 + assert_eq!(store_records.len(), 0); 1216 + 1217 + let pg_colls = f.pg.repo.list_collections(uid).await.unwrap(); 1218 + let store_colls = f.store.repo.list_collections(uid).await.unwrap(); 1219 + assert_eq!(pg_colls.len(), 0); 1220 + assert_eq!(store_colls.len(), 0); 1221 + 1222 + let pg_count = f.pg.repo.count_records(uid).await.unwrap(); 1223 + let store_count = f.store.repo.count_records(uid).await.unwrap(); 1224 + assert_eq!(pg_count, 0); 1225 + assert_eq!(store_count, 0); 1226 + 1227 + let pg_blobs = f.pg.blob.list_blobs_by_user(uid, None, 100).await.unwrap(); 1228 + let store_blobs = f 1229 + .store 1230 + .blob 1231 + .list_blobs_by_user(uid, None, 100) 1232 + .await 1233 + .unwrap(); 1234 + assert_eq!(pg_blobs.len(), 0); 1235 + assert_eq!(store_blobs.len(), 0); 1236 + 1237 + let nonexistent_cid = test_cid(255); 1238 + let pg_meta = f.pg.blob.get_blob_metadata(&nonexistent_cid).await.unwrap(); 1239 + let store_meta = f 1240 + .store 1241 + .blob 1242 + .get_blob_metadata(&nonexistent_cid) 1243 + .await 1244 + .unwrap(); 1245 + assert_eq!(pg_meta.is_none(), store_meta.is_none()); 1246 + } 1247 + 1248 + #[tokio::test] 1249 + async fn parity_deletion_requests() { 1250 + let f = ParityFixture::new().await; 1251 + let did = test_did("delreq"); 1252 + let token = format!("del-token-{}", Uuid::new_v4()); 1253 + let expires = chrono::Utc::now() + chrono::Duration::hours(24); 1254 + 1255 + f.pg.infra 1256 + .create_deletion_request(&token, &did, expires) 1257 + .await 1258 + .unwrap(); 1259 + f.store 1260 + .infra 1261 + .create_deletion_request(&token, &did, expires) 1262 + .await 1263 + .unwrap(); 1264 + 1265 + let pg_req = f.pg.infra.get_deletion_request(&token).await.unwrap(); 1266 + let store_req = f.store.infra.get_deletion_request(&token).await.unwrap(); 1267 + assert!(pg_req.is_some()); 1268 + assert!(store_req.is_some()); 1269 + assert_eq!( 1270 + pg_req.as_ref().unwrap().did, 1271 + store_req.as_ref().unwrap().did 1272 + ); 1273 + 1274 + let pg_by_did = f.pg.infra.get_deletion_request_by_did(&did).await.unwrap(); 1275 + let store_by_did = f 1276 + .store 1277 + .infra 1278 + .get_deletion_request_by_did(&did) 1279 + .await 1280 + .unwrap(); 1281 + assert!(pg_by_did.is_some()); 1282 + assert!(store_by_did.is_some()); 1283 + assert_eq!(pg_by_did.unwrap().token, store_by_did.unwrap().token); 1284 + 1285 + f.pg.infra.delete_deletion_request(&token).await.unwrap(); 1286 + f.store.infra.delete_deletion_request(&token).await.unwrap(); 1287 + 1288 + let pg_gone = f.pg.infra.get_deletion_request(&token).await.unwrap(); 1289 + let store_gone = f.store.infra.get_deletion_request(&token).await.unwrap(); 1290 + assert!(pg_gone.is_none()); 1291 + assert!(store_gone.is_none()); 1292 + } 1293 + 1294 + #[tokio::test] 1295 + async fn parity_signing_key_reservation() { 1296 + let f = ParityFixture::new().await; 1297 + let did = test_did("sigkey"); 1298 + let expires = chrono::Utc::now() + chrono::Duration::hours(1); 1299 + let pub_key = format!("did:key:z6Mk{}", Uuid::new_v4().simple()); 1300 + let priv_bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; 1301 + 1302 + f.pg.infra 1303 + .reserve_signing_key(Some(&did), &pub_key, &priv_bytes, expires) 1304 + .await 1305 + .unwrap(); 1306 + f.store 1307 + .infra 1308 + .reserve_signing_key(Some(&did), &pub_key, &priv_bytes, expires) 1309 + .await 1310 + .unwrap(); 1311 + 1312 + let pg_key = f.pg.infra.get_reserved_signing_key(&pub_key).await.unwrap(); 1313 + let store_key = f 1314 + .store 1315 + .infra 1316 + .get_reserved_signing_key(&pub_key) 1317 + .await 1318 + .unwrap(); 1319 + assert!(pg_key.is_some()); 1320 + assert!(store_key.is_some()); 1321 + assert_eq!( 1322 + pg_key.unwrap().private_key_bytes, 1323 + store_key.unwrap().private_key_bytes 1324 + ); 1325 + 1326 + let pg_full = 1327 + f.pg.infra 1328 + .get_reserved_signing_key_full(&pub_key) 1329 + .await 1330 + .unwrap(); 1331 + let store_full = f 1332 + .store 1333 + .infra 1334 + .get_reserved_signing_key_full(&pub_key) 1335 + .await 1336 + .unwrap(); 1337 + assert!(pg_full.is_some()); 1338 + assert!(store_full.is_some()); 1339 + let pg_f = pg_full.unwrap(); 1340 + let store_f = store_full.unwrap(); 1341 + assert_eq!(pg_f.public_key_did_key, store_f.public_key_did_key); 1342 + assert_eq!(pg_f.did, store_f.did); 1343 + } 1344 + 1345 + #[tokio::test] 1346 + async fn parity_repo_root_operations() { 1347 + let f = ParityFixture::new().await; 1348 + let uid = Uuid::new_v4(); 1349 + let did = test_did("root"); 1350 + let handle = test_handle("root"); 1351 + let root_cid = test_cid(0); 1352 + 1353 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 1354 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 1355 + 1356 + let pg_root = f.pg.repo.get_repo_root_by_did(&did).await.unwrap(); 1357 + let store_root = f.store.repo.get_repo_root_by_did(&did).await.unwrap(); 1358 + assert_eq!(pg_root, store_root); 1359 + 1360 + let new_root = test_cid(99); 1361 + f.pg.repo 1362 + .update_repo_root(uid, &new_root, "rev1") 1363 + .await 1364 + .unwrap(); 1365 + f.store 1366 + .repo 1367 + .update_repo_root(uid, &new_root, "rev1") 1368 + .await 1369 + .unwrap(); 1370 + 1371 + let pg_updated = f.pg.repo.get_repo_root_by_did(&did).await.unwrap(); 1372 + let store_updated = f.store.repo.get_repo_root_by_did(&did).await.unwrap(); 1373 + assert_eq!(pg_updated, store_updated); 1374 + assert_eq!(pg_updated.unwrap().as_str(), new_root.as_str()); 1375 + 1376 + let pg_info = f.pg.repo.get_repo(uid).await.unwrap().unwrap(); 1377 + let store_info = f.store.repo.get_repo(uid).await.unwrap().unwrap(); 1378 + assert_eq!(pg_info.repo_rev, store_info.repo_rev); 1379 + assert_eq!( 1380 + pg_info.repo_root_cid.as_str(), 1381 + store_info.repo_root_cid.as_str() 1382 + ); 1383 + } 1384 + 1385 + #[tokio::test] 1386 + async fn parity_delete_all_records() { 1387 + let f = ParityFixture::new().await; 1388 + let uid = Uuid::new_v4(); 1389 + let did = test_did("delall"); 1390 + let handle = test_handle("delall"); 1391 + let root_cid = test_cid(0); 1392 + let collection = test_nsid("post"); 1393 + 1394 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 1395 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 1396 + 1397 + let records: Vec<(Rkey, CidLink)> = (0u8..5) 1398 + .map(|i| (test_rkey(&format!("3l{:02}aaaaaaaaa", i)), test_cid(i + 1))) 1399 + .collect(); 1400 + 1401 + seed_records(&f.pg, uid, &collection, &records).await; 1402 + seed_records(&f.store, uid, &collection, &records).await; 1403 + 1404 + f.pg.repo.delete_all_records(uid).await.unwrap(); 1405 + f.store.repo.delete_all_records(uid).await.unwrap(); 1406 + 1407 + let pg_count = f.pg.repo.count_records(uid).await.unwrap(); 1408 + let store_count = f.store.repo.count_records(uid).await.unwrap(); 1409 + assert_eq!(pg_count, 0); 1410 + assert_eq!(store_count, 0); 1411 + 1412 + let pg_colls = f.pg.repo.list_collections(uid).await.unwrap(); 1413 + let store_colls = f.store.repo.list_collections(uid).await.unwrap(); 1414 + assert_eq!(pg_colls.len(), 0); 1415 + assert_eq!(store_colls.len(), 0); 1416 + } 1417 + 1418 + #[tokio::test] 1419 + async fn parity_plc_tokens() { 1420 + let f = ParityFixture::new().await; 1421 + let uid = Uuid::new_v4(); 1422 + let did = test_did("plctok"); 1423 + let handle = test_handle("plctok"); 1424 + let root_cid = test_cid(0); 1425 + 1426 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 1427 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 1428 + 1429 + let token = format!("plc-{}", Uuid::new_v4()); 1430 + let expires = chrono::Utc::now() + chrono::Duration::hours(1); 1431 + 1432 + f.pg.infra 1433 + .insert_plc_token(uid, &token, expires) 1434 + .await 1435 + .unwrap(); 1436 + f.store 1437 + .infra 1438 + .insert_plc_token(uid, &token, expires) 1439 + .await 1440 + .unwrap(); 1441 + 1442 + let pg_expiry = f.pg.infra.get_plc_token_expiry(uid, &token).await.unwrap(); 1443 + let store_expiry = f 1444 + .store 1445 + .infra 1446 + .get_plc_token_expiry(uid, &token) 1447 + .await 1448 + .unwrap(); 1449 + assert!(pg_expiry.is_some()); 1450 + assert!(store_expiry.is_some()); 1451 + 1452 + let pg_by_did = f.pg.infra.get_plc_tokens_by_did(&did).await.unwrap(); 1453 + let store_by_did = f.store.infra.get_plc_tokens_by_did(&did).await.unwrap(); 1454 + assert_eq!(pg_by_did.len(), store_by_did.len()); 1455 + 1456 + let pg_count = f.pg.infra.count_plc_tokens_by_did(&did).await.unwrap(); 1457 + let store_count = f.store.infra.count_plc_tokens_by_did(&did).await.unwrap(); 1458 + assert_eq!(pg_count, store_count); 1459 + assert_eq!(pg_count, 1); 1460 + 1461 + f.pg.infra.delete_plc_token(uid, &token).await.unwrap(); 1462 + f.store.infra.delete_plc_token(uid, &token).await.unwrap(); 1463 + 1464 + let pg_gone = f.pg.infra.get_plc_token_expiry(uid, &token).await.unwrap(); 1465 + let store_gone = f 1466 + .store 1467 + .infra 1468 + .get_plc_token_expiry(uid, &token) 1469 + .await 1470 + .unwrap(); 1471 + assert!(pg_gone.is_none()); 1472 + assert!(store_gone.is_none()); 1473 + } 1474 + 1475 + #[tokio::test] 1476 + async fn parity_blob_delete_and_takedown() { 1477 + let f = ParityFixture::new().await; 1478 + let uid = Uuid::new_v4(); 1479 + let did = test_did("blobdel"); 1480 + let handle = test_handle("blobdel"); 1481 + let root_cid = test_cid(0); 1482 + 1483 + seed_repo(&f.pg, &did, &handle, &root_cid, uid).await; 1484 + seed_repo(&f.store, &did, &handle, &root_cid, uid).await; 1485 + 1486 + let cid = test_cid(180); 1487 + f.pg.blob 1488 + .insert_blob(&cid, "image/png", 1024, uid, "blobs/td.png") 1489 + .await 1490 + .unwrap(); 1491 + f.store 1492 + .blob 1493 + .insert_blob(&cid, "image/png", 1024, uid, "blobs/td.png") 1494 + .await 1495 + .unwrap(); 1496 + 1497 + let pg_td = 1498 + f.pg.blob 1499 + .update_blob_takedown(&cid, Some("mod-action-1")) 1500 + .await 1501 + .unwrap(); 1502 + let store_td = f 1503 + .store 1504 + .blob 1505 + .update_blob_takedown(&cid, Some("mod-action-1")) 1506 + .await 1507 + .unwrap(); 1508 + assert_eq!(pg_td, store_td); 1509 + 1510 + let pg_with_td = f.pg.blob.get_blob_with_takedown(&cid).await.unwrap(); 1511 + let store_with_td = f.store.blob.get_blob_with_takedown(&cid).await.unwrap(); 1512 + assert_eq!( 1513 + pg_with_td.as_ref().map(|b| b.takedown_ref.as_deref()), 1514 + store_with_td.as_ref().map(|b| b.takedown_ref.as_deref()) 1515 + ); 1516 + 1517 + f.pg.blob.delete_blob_by_cid(&cid).await.unwrap(); 1518 + f.store.blob.delete_blob_by_cid(&cid).await.unwrap(); 1519 + 1520 + let pg_meta = f.pg.blob.get_blob_metadata(&cid).await.unwrap(); 1521 + let store_meta = f.store.blob.get_blob_metadata(&cid).await.unwrap(); 1522 + assert!(pg_meta.is_none()); 1523 + assert!(store_meta.is_none()); 1524 + }
+12 -12
crates/tranquil-pds/tests/whole_story.rs
··· 177 177 .expect("Request delete failed"); 178 178 assert_eq!(request_delete_res.status(), StatusCode::OK); 179 179 180 - let pool = get_test_db_pool().await; 181 - let row = sqlx::query!( 182 - "SELECT token FROM account_deletion_requests WHERE did = $1", 183 - did 184 - ) 185 - .fetch_one(pool) 186 - .await 187 - .expect("Failed to get deletion token"); 180 + let repos = get_test_repos().await; 181 + let deletion_request = repos 182 + .infra 183 + .get_deletion_request_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 184 + .await 185 + .unwrap() 186 + .unwrap(); 188 187 189 188 let final_delete_res = client 190 189 .post(format!("{}/xrpc/com.atproto.server.deleteAccount", base)) 191 190 .json(&json!({ 192 191 "did": did, 193 192 "password": password, 194 - "token": row.token 193 + "token": deletion_request.token 195 194 })) 196 195 .send() 197 196 .await 198 197 .expect("Final delete failed"); 199 198 assert_eq!(final_delete_res.status(), StatusCode::OK); 200 199 201 - let user_gone = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 202 - .fetch_optional(pool) 200 + let user_gone = repos 201 + .user 202 + .get_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) 203 203 .await 204 - .expect("Failed to check user"); 204 + .unwrap(); 205 205 assert!(user_gone.is_none(), "User should be deleted"); 206 206 } 207 207
+2 -2
crates/tranquil-server/src/main.rs
··· 114 114 let signal_sender = if tranquil_config::get().signal.enabled { 115 115 let slot = Arc::new(tranquil_signal::SignalSlot::default()); 116 116 state = state.with_signal_sender(slot.clone()); 117 - if let Some(client) = 118 - tranquil_signal::SignalClient::from_pool(&state.repos.pool, shutdown.clone()).await 117 + if let Some(provider) = &state.signal_store_provider 118 + && let Some(client) = provider.load_signal_client(shutdown.clone()).await 119 119 { 120 120 slot.set_client(client).await; 121 121 info!("Signal device already linked");
+4
crates/tranquil-signal/Cargo.toml
··· 4 4 edition.workspace = true 5 5 license.workspace = true 6 6 7 + [features] 8 + fjall-store = ["dep:fjall"] 9 + 7 10 [dependencies] 8 11 presage = { workspace = true } 9 12 async-trait = { workspace = true } 10 13 chrono = { workspace = true } 14 + fjall = { version = "3", optional = true } 11 15 sqlx = { workspace = true } 12 16 tracing = { workspace = true } 13 17 tokio = { workspace = true }
+39 -12
crates/tranquil-signal/src/client.rs
··· 7 7 use presage::libsignal_service::configuration::SignalServers; 8 8 use presage::manager::Registered; 9 9 use presage::proto::DataMessage; 10 + use presage::store::Store; 10 11 use sqlx::PgPool; 11 12 use tokio::sync::{RwLock, mpsc, oneshot}; 12 13 use tokio_util::sync::CancellationToken; ··· 190 191 #[derive(Debug, thiserror::Error)] 191 192 pub enum SignalError { 192 193 #[error("store: {0}")] 193 - Store(#[from] crate::store::PgStoreError), 194 + Store(String), 194 195 #[error("presage: {0}")] 195 196 Presage(String), 196 197 #[error("username lookup failed: {0}")] ··· 209 210 Runtime(String), 210 211 } 211 212 212 - type Manager = presage::Manager<PgSignalStore, Registered>; 213 + impl From<crate::store::PgStoreError> for SignalError { 214 + fn from(e: crate::store::PgStoreError) -> Self { 215 + Self::Store(e.to_string()) 216 + } 217 + } 213 218 214 219 struct SendRequest { 215 220 recipient: SignalUsername, ··· 311 316 } 312 317 313 318 impl SignalClient { 314 - fn from_manager(manager: Manager, shutdown: CancellationToken) -> Result<Self, SignalError> { 319 + fn from_manager<S: Store>( 320 + manager: presage::Manager<S, Registered>, 321 + shutdown: CancellationToken, 322 + ) -> Result<Self, SignalError> { 315 323 let (tx, rx) = mpsc::channel::<SendRequest>(64); 316 324 317 325 spawn_signal_thread("signal-worker", move || { ··· 322 330 Ok(Self { tx }) 323 331 } 324 332 325 - pub async fn from_pool(db: &PgPool, shutdown: CancellationToken) -> Option<Self> { 326 - let store = PgSignalStore::new(db.clone()); 333 + pub async fn from_store<S: Store>(store: S, shutdown: CancellationToken) -> Option<Self> { 327 334 let (init_tx, init_rx) = oneshot::channel(); 328 335 329 336 spawn_signal_thread("signal-init", move || { ··· 348 355 .ok() 349 356 } 350 357 351 - async fn worker_loop( 352 - mut manager: Manager, 358 + pub async fn from_pool(db: &PgPool, shutdown: CancellationToken) -> Option<Self> { 359 + Self::from_store(PgSignalStore::new(db.clone()), shutdown).await 360 + } 361 + 362 + async fn worker_loop<S: Store>( 363 + mut manager: presage::Manager<S, Registered>, 353 364 mut rx: mpsc::Receiver<SendRequest>, 354 365 shutdown: CancellationToken, 355 366 ) { ··· 391 402 } 392 403 } 393 404 394 - async fn handle_send( 395 - manager: &mut Manager, 405 + async fn handle_send<S: Store>( 406 + manager: &mut presage::Manager<S, Registered>, 396 407 recipient: &SignalUsername, 397 408 message: &MessageBody, 398 409 ) -> Result<(), SignalError> { ··· 444 455 .map_err(|_| SignalError::Runtime("signal worker dropped request".into()))? 445 456 } 446 457 447 - pub async fn link_device( 448 - db: &PgPool, 458 + pub async fn link_device_with_store<S: Store>( 459 + store: S, 449 460 device_name: DeviceName, 450 461 shutdown: CancellationToken, 451 462 link_cancel: CancellationToken, ··· 457 468 )); 458 469 } 459 470 460 - let store = PgSignalStore::new(db.clone()); 461 471 let (url_tx, url_rx) = oneshot::channel::<Result<Url, SignalError>>(); 462 472 let (done_tx, done_rx) = oneshot::channel::<Result<SignalClient, SignalError>>(); 463 473 ··· 537 547 url, 538 548 completion: done_rx, 539 549 }) 550 + } 551 + 552 + pub async fn link_device( 553 + db: &PgPool, 554 + device_name: DeviceName, 555 + shutdown: CancellationToken, 556 + link_cancel: CancellationToken, 557 + linking_flag: Arc<AtomicBool>, 558 + ) -> Result<LinkResult, SignalError> { 559 + Self::link_device_with_store( 560 + PgSignalStore::new(db.clone()), 561 + device_name, 562 + shutdown, 563 + link_cancel, 564 + linking_flag, 565 + ) 566 + .await 540 567 } 541 568 }
+1157
crates/tranquil-signal/src/fjall_store.rs
··· 1 + use std::ops::RangeBounds; 2 + use std::sync::Arc; 3 + 4 + use async_trait::async_trait; 5 + use fjall::{Database, Keyspace}; 6 + use presage::{ 7 + AvatarBytes, 8 + libsignal_service::{ 9 + Profile, 10 + pre_keys::{KyberPreKeyStoreExt, PreKeysStore}, 11 + prelude::{Content, MasterKey, ProfileKey, SessionStoreExt, Uuid}, 12 + protocol::{ 13 + CiphertextMessageType, DeviceId, Direction, GenericSignedPreKey, IdentityChange, 14 + IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyRecord, 15 + KyberPreKeyStore, PreKeyId, PreKeyRecord, PreKeyStore, ProtocolAddress, ProtocolStore, 16 + PublicKey, SenderCertificate, SenderKeyRecord, SenderKeyStore, ServiceId, 17 + SessionRecord, SessionStore, SignalProtocolError, SignedPreKeyId, SignedPreKeyRecord, 18 + SignedPreKeyStore, 19 + }, 20 + push_service::DEFAULT_DEVICE_ID, 21 + zkgroup::GroupMasterKeyBytes, 22 + }, 23 + manager::RegistrationData, 24 + model::{contacts::Contact, groups::Group}, 25 + store::{ContentsStore, StateStore, StickerPack, Store, Thread}, 26 + }; 27 + use tracing::warn; 28 + 29 + use crate::{DeviceName, LinkResult, SignalClient, SignalError}; 30 + 31 + #[derive(Debug, thiserror::Error)] 32 + pub enum FjallStoreError { 33 + #[error("fjall: {0}")] 34 + Fjall(#[from] fjall::Error), 35 + #[error("json: {0}")] 36 + Json(#[from] serde_json::Error), 37 + #[error("protocol: {0}")] 38 + Protocol(#[from] SignalProtocolError), 39 + #[error("not found: {0}")] 40 + NotFound(String), 41 + } 42 + 43 + impl presage::store::StoreError for FjallStoreError {} 44 + 45 + #[derive(Clone)] 46 + pub struct FjallSignalStore { 47 + db: Database, 48 + ks: Keyspace, 49 + } 50 + 51 + #[derive(Clone)] 52 + pub struct FjallProtocolStore { 53 + store: FjallSignalStore, 54 + identity: IdentityType, 55 + } 56 + 57 + #[derive(Debug, Clone, Copy)] 58 + enum IdentityType { 59 + Aci, 60 + Pni, 61 + } 62 + 63 + impl IdentityType { 64 + fn tag(self) -> u8 { 65 + match self { 66 + Self::Aci => b'a', 67 + Self::Pni => b'p', 68 + } 69 + } 70 + } 71 + 72 + fn kv_key(name: &[u8]) -> Vec<u8> { 73 + let mut k = Vec::with_capacity(1 + name.len()); 74 + k.push(b'K'); 75 + k.extend_from_slice(name); 76 + k 77 + } 78 + 79 + fn session_key(identity: IdentityType, address: &str, device_id: u32) -> Vec<u8> { 80 + let mut k = Vec::with_capacity(2 + address.len() + 1 + 4); 81 + k.push(b'S'); 82 + k.push(identity.tag()); 83 + k.extend_from_slice(address.as_bytes()); 84 + k.push(0); 85 + k.extend_from_slice(&device_id.to_be_bytes()); 86 + k 87 + } 88 + 89 + fn session_prefix(identity: IdentityType, address: &str) -> Vec<u8> { 90 + let mut k = Vec::with_capacity(2 + address.len() + 1); 91 + k.push(b'S'); 92 + k.push(identity.tag()); 93 + k.extend_from_slice(address.as_bytes()); 94 + k.push(0); 95 + k 96 + } 97 + 98 + fn identity_rec_key(identity: IdentityType, address: &str) -> Vec<u8> { 99 + let mut k = Vec::with_capacity(2 + address.len()); 100 + k.push(b'I'); 101 + k.push(identity.tag()); 102 + k.extend_from_slice(address.as_bytes()); 103 + k 104 + } 105 + 106 + fn pre_key_key(identity: IdentityType, id: u32) -> Vec<u8> { 107 + let mut k = vec![b'P', identity.tag()]; 108 + k.extend_from_slice(&id.to_be_bytes()); 109 + k 110 + } 111 + 112 + fn pre_key_prefix(identity: IdentityType) -> Vec<u8> { 113 + vec![b'P', identity.tag()] 114 + } 115 + 116 + fn signed_pre_key_key(identity: IdentityType, id: u32) -> Vec<u8> { 117 + let mut k = vec![b'G', identity.tag()]; 118 + k.extend_from_slice(&id.to_be_bytes()); 119 + k 120 + } 121 + 122 + fn signed_pre_key_prefix(identity: IdentityType) -> Vec<u8> { 123 + vec![b'G', identity.tag()] 124 + } 125 + 126 + fn kyber_key(identity: IdentityType, id: u32) -> Vec<u8> { 127 + let mut k = vec![b'Q', identity.tag()]; 128 + k.extend_from_slice(&id.to_be_bytes()); 129 + k 130 + } 131 + 132 + fn kyber_prefix(identity: IdentityType) -> Vec<u8> { 133 + vec![b'Q', identity.tag()] 134 + } 135 + 136 + fn sender_key_key( 137 + identity: IdentityType, 138 + address: &str, 139 + device_id: u32, 140 + distribution_id: Uuid, 141 + ) -> Vec<u8> { 142 + let mut k = Vec::with_capacity(2 + address.len() + 1 + 4 + 16); 143 + k.push(b'N'); 144 + k.push(identity.tag()); 145 + k.extend_from_slice(address.as_bytes()); 146 + k.push(0); 147 + k.extend_from_slice(&device_id.to_be_bytes()); 148 + k.extend_from_slice(distribution_id.as_bytes()); 149 + k 150 + } 151 + 152 + fn base_key_seen_key( 153 + identity: IdentityType, 154 + kyber_id: u32, 155 + signed_id: u32, 156 + base_key: &[u8], 157 + ) -> Vec<u8> { 158 + let mut k = Vec::with_capacity(2 + 4 + 4 + base_key.len()); 159 + k.push(b'B'); 160 + k.push(identity.tag()); 161 + k.extend_from_slice(&kyber_id.to_be_bytes()); 162 + k.extend_from_slice(&signed_id.to_be_bytes()); 163 + k.extend_from_slice(base_key); 164 + k 165 + } 166 + 167 + fn profile_key_key(uuid: &Uuid) -> Vec<u8> { 168 + let mut k = Vec::with_capacity(1 + 16); 169 + k.push(b'R'); 170 + k.extend_from_slice(uuid.as_bytes()); 171 + k 172 + } 173 + 174 + const KYBER_META_SIZE: usize = 1 + 8; 175 + 176 + fn encode_kyber_value(record: &[u8], is_last_resort: bool, stale_at_ms: Option<i64>) -> Vec<u8> { 177 + let mut v = Vec::with_capacity(KYBER_META_SIZE + record.len()); 178 + v.push(u8::from(is_last_resort)); 179 + v.extend_from_slice(&stale_at_ms.unwrap_or(0).to_be_bytes()); 180 + v.extend_from_slice(record); 181 + v 182 + } 183 + 184 + fn decode_kyber_value(data: &[u8]) -> Option<(bool, Option<i64>, &[u8])> { 185 + (data.len() >= KYBER_META_SIZE).then_some(())?; 186 + let is_last_resort = data[0] != 0; 187 + let stale_ms = i64::from_be_bytes(data[1..9].try_into().ok()?); 188 + let stale_at = (stale_ms != 0).then_some(stale_ms); 189 + Some((is_last_resort, stale_at, &data[KYBER_META_SIZE..])) 190 + } 191 + 192 + fn into_protocol_err<E: std::fmt::Display>(e: E) -> SignalProtocolError { 193 + SignalProtocolError::InvalidState("fjall", e.to_string()) 194 + } 195 + 196 + fn guard_to_kv(guard: fjall::Guard) -> Result<fjall::KvPair, fjall::Error> { 197 + guard.into_inner() 198 + } 199 + 200 + fn max_id_in_prefix(ks: &Keyspace, prefix: &[u8]) -> Result<Option<u32>, SignalProtocolError> { 201 + let mut max: Option<u32> = None; 202 + ks.prefix(prefix).try_for_each(|guard| { 203 + let (key_bytes, _) = guard_to_kv(guard).map_err(into_protocol_err)?; 204 + let id_offset = prefix.len(); 205 + if key_bytes.len() >= id_offset + 4 { 206 + let id = u32::from_be_bytes( 207 + key_bytes[id_offset..id_offset + 4] 208 + .try_into() 209 + .map_err(into_protocol_err)?, 210 + ); 211 + max = Some(max.map_or(id, |m| m.max(id))); 212 + } 213 + Ok::<(), SignalProtocolError>(()) 214 + })?; 215 + Ok(max) 216 + } 217 + 218 + fn next_id_from_max(max_id: Option<u32>) -> Result<u32, SignalProtocolError> { 219 + match max_id { 220 + None => Ok(1), 221 + Some(id) => id.checked_add(1).ok_or_else(|| { 222 + SignalProtocolError::InvalidState( 223 + "pre key id space exhausted", 224 + format!("max id {id} has no successor"), 225 + ) 226 + }), 227 + } 228 + } 229 + 230 + impl FjallSignalStore { 231 + pub fn new(db: Database, ks: Keyspace) -> Self { 232 + Self { db, ks } 233 + } 234 + 235 + pub fn is_linked(&self) -> Result<bool, FjallStoreError> { 236 + Ok(self.ks.get(kv_key(b"registration"))?.is_some()) 237 + } 238 + 239 + pub fn clear_all(&self) -> Result<(), FjallStoreError> { 240 + let mut batch = self.db.batch(); 241 + self.ks.prefix([]).try_for_each(|guard| { 242 + let (key, _) = guard.into_inner()?; 243 + batch.remove(&self.ks, key.as_ref()); 244 + Ok::<(), fjall::Error>(()) 245 + })?; 246 + batch.commit()?; 247 + Ok(()) 248 + } 249 + 250 + fn set_identity_key_pair( 251 + &self, 252 + identity: IdentityType, 253 + key_pair: IdentityKeyPair, 254 + ) -> Result<(), FjallStoreError> { 255 + let key_name = match identity { 256 + IdentityType::Aci => b"identity_keypair_aci".as_slice(), 257 + IdentityType::Pni => b"identity_keypair_pni".as_slice(), 258 + }; 259 + let serialized = key_pair.serialize(); 260 + self.ks.insert(kv_key(key_name), &*serialized)?; 261 + Ok(()) 262 + } 263 + } 264 + 265 + impl Store for FjallSignalStore { 266 + type Error = FjallStoreError; 267 + type AciStore = FjallProtocolStore; 268 + type PniStore = FjallProtocolStore; 269 + 270 + async fn clear(&mut self) -> Result<(), FjallStoreError> { 271 + self.clear_all() 272 + } 273 + 274 + fn aci_protocol_store(&self) -> Self::AciStore { 275 + FjallProtocolStore { 276 + store: self.clone(), 277 + identity: IdentityType::Aci, 278 + } 279 + } 280 + 281 + fn pni_protocol_store(&self) -> Self::PniStore { 282 + FjallProtocolStore { 283 + store: self.clone(), 284 + identity: IdentityType::Pni, 285 + } 286 + } 287 + } 288 + 289 + impl StateStore for FjallSignalStore { 290 + type StateStoreError = FjallStoreError; 291 + 292 + async fn load_registration_data(&self) -> Result<Option<RegistrationData>, FjallStoreError> { 293 + self.ks 294 + .get(kv_key(b"registration"))? 295 + .map(|v| serde_json::from_slice(&v)) 296 + .transpose() 297 + .map_err(From::from) 298 + } 299 + 300 + async fn save_registration_data( 301 + &mut self, 302 + state: &RegistrationData, 303 + ) -> Result<(), FjallStoreError> { 304 + let value = serde_json::to_vec(state)?; 305 + self.ks.insert(kv_key(b"registration"), &value)?; 306 + Ok(()) 307 + } 308 + 309 + async fn is_registered(&self) -> bool { 310 + self.ks 311 + .get(kv_key(b"registration")) 312 + .ok() 313 + .flatten() 314 + .is_some() 315 + } 316 + 317 + async fn clear_registration(&mut self) -> Result<(), FjallStoreError> { 318 + let protocol_tags: &[u8] = b"SIPGQNB"; 319 + let mut batch = self.db.batch(); 320 + batch.remove(&self.ks, kv_key(b"registration")); 321 + self.ks.prefix([]).try_for_each(|guard| { 322 + let (key, _) = guard.into_inner()?; 323 + if key.first().is_some_and(|b| protocol_tags.contains(b)) { 324 + batch.remove(&self.ks, key.as_ref()); 325 + } 326 + Ok::<(), fjall::Error>(()) 327 + })?; 328 + batch.commit()?; 329 + Ok(()) 330 + } 331 + 332 + async fn set_aci_identity_key_pair( 333 + &self, 334 + key_pair: IdentityKeyPair, 335 + ) -> Result<(), FjallStoreError> { 336 + self.set_identity_key_pair(IdentityType::Aci, key_pair) 337 + } 338 + 339 + async fn set_pni_identity_key_pair( 340 + &self, 341 + key_pair: IdentityKeyPair, 342 + ) -> Result<(), FjallStoreError> { 343 + self.set_identity_key_pair(IdentityType::Pni, key_pair) 344 + } 345 + 346 + async fn sender_certificate(&self) -> Result<Option<SenderCertificate>, FjallStoreError> { 347 + self.ks 348 + .get(kv_key(b"sender_certificate"))? 349 + .map(|v| SenderCertificate::deserialize(&v)) 350 + .transpose() 351 + .map_err(From::from) 352 + } 353 + 354 + async fn save_sender_certificate( 355 + &self, 356 + certificate: &SenderCertificate, 357 + ) -> Result<(), FjallStoreError> { 358 + let serialized = certificate.serialized()?; 359 + self.ks.insert(kv_key(b"sender_certificate"), serialized)?; 360 + Ok(()) 361 + } 362 + 363 + async fn fetch_master_key(&self) -> Result<Option<MasterKey>, FjallStoreError> { 364 + self.ks 365 + .get(kv_key(b"master_key"))? 366 + .map(|v| MasterKey::from_slice(&v)) 367 + .transpose() 368 + .map_err(|_| FjallStoreError::NotFound("master key has wrong length".into())) 369 + } 370 + 371 + async fn store_master_key( 372 + &self, 373 + master_key: Option<&MasterKey>, 374 + ) -> Result<(), FjallStoreError> { 375 + match master_key { 376 + Some(k) => self.ks.insert(kv_key(b"master_key"), k.inner)?, 377 + None => self.ks.remove(kv_key(b"master_key"))?, 378 + } 379 + Ok(()) 380 + } 381 + } 382 + 383 + impl ProtocolStore for FjallProtocolStore {} 384 + 385 + #[async_trait(?Send)] 386 + impl SessionStore for FjallProtocolStore { 387 + async fn load_session( 388 + &self, 389 + address: &ProtocolAddress, 390 + ) -> Result<Option<SessionRecord>, SignalProtocolError> { 391 + let key = session_key( 392 + self.identity, 393 + address.name(), 394 + u32::from(address.device_id()), 395 + ); 396 + self.store 397 + .ks 398 + .get(key) 399 + .map_err(into_protocol_err)? 400 + .map(|v| SessionRecord::deserialize(&v)) 401 + .transpose() 402 + } 403 + 404 + async fn store_session( 405 + &mut self, 406 + address: &ProtocolAddress, 407 + record: &SessionRecord, 408 + ) -> Result<(), SignalProtocolError> { 409 + let key = session_key( 410 + self.identity, 411 + address.name(), 412 + u32::from(address.device_id()), 413 + ); 414 + let serialized = record.serialize()?; 415 + self.store 416 + .ks 417 + .insert(key, serialized.as_slice()) 418 + .map_err(into_protocol_err) 419 + } 420 + } 421 + 422 + #[async_trait(?Send)] 423 + impl SessionStoreExt for FjallProtocolStore { 424 + async fn get_sub_device_sessions( 425 + &self, 426 + name: &ServiceId, 427 + ) -> Result<Vec<DeviceId>, SignalProtocolError> { 428 + let address = name.raw_uuid().to_string(); 429 + let default_device = u32::from(*DEFAULT_DEVICE_ID); 430 + let prefix = session_prefix(self.identity, &address); 431 + let prefix_len = prefix.len(); 432 + 433 + let mut devices = Vec::new(); 434 + self.store.ks.prefix(prefix).try_for_each(|guard| { 435 + let (key, _) = guard_to_kv(guard).map_err(into_protocol_err)?; 436 + if key.len() >= prefix_len + 4 { 437 + let id = u32::from_be_bytes( 438 + key[prefix_len..prefix_len + 4] 439 + .try_into() 440 + .map_err(into_protocol_err)?, 441 + ); 442 + if id != default_device 443 + && let Ok(byte) = u8::try_from(id) 444 + && let Ok(device_id) = DeviceId::new(byte) 445 + { 446 + devices.push(device_id); 447 + } 448 + } 449 + Ok::<(), SignalProtocolError>(()) 450 + })?; 451 + Ok(devices) 452 + } 453 + 454 + async fn delete_session(&self, address: &ProtocolAddress) -> Result<(), SignalProtocolError> { 455 + let key = session_key( 456 + self.identity, 457 + address.name(), 458 + u32::from(address.device_id()), 459 + ); 460 + self.store.ks.remove(key).map_err(into_protocol_err) 461 + } 462 + 463 + async fn delete_all_sessions(&self, name: &ServiceId) -> Result<usize, SignalProtocolError> { 464 + let address = name.raw_uuid().to_string(); 465 + let prefix = session_prefix(self.identity, &address); 466 + let mut batch = self.store.db.batch(); 467 + let mut count = 0usize; 468 + self.store.ks.prefix(prefix).try_for_each(|guard| { 469 + let (key, _) = guard_to_kv(guard).map_err(into_protocol_err)?; 470 + batch.remove(&self.store.ks, key.as_ref()); 471 + count = count.saturating_add(1); 472 + Ok::<(), SignalProtocolError>(()) 473 + })?; 474 + batch.commit().map_err(into_protocol_err)?; 475 + Ok(count) 476 + } 477 + } 478 + 479 + #[async_trait(?Send)] 480 + impl PreKeyStore for FjallProtocolStore { 481 + async fn get_pre_key(&self, prekey_id: PreKeyId) -> Result<PreKeyRecord, SignalProtocolError> { 482 + let key = pre_key_key(self.identity, u32::from(prekey_id)); 483 + let data = self 484 + .store 485 + .ks 486 + .get(key) 487 + .map_err(into_protocol_err)? 488 + .ok_or(SignalProtocolError::InvalidPreKeyId)?; 489 + PreKeyRecord::deserialize(&data) 490 + } 491 + 492 + async fn save_pre_key( 493 + &mut self, 494 + prekey_id: PreKeyId, 495 + record: &PreKeyRecord, 496 + ) -> Result<(), SignalProtocolError> { 497 + let key = pre_key_key(self.identity, u32::from(prekey_id)); 498 + let serialized = record.serialize()?; 499 + self.store 500 + .ks 501 + .insert(key, serialized.as_slice()) 502 + .map_err(into_protocol_err) 503 + } 504 + 505 + async fn remove_pre_key(&mut self, prekey_id: PreKeyId) -> Result<(), SignalProtocolError> { 506 + let key = pre_key_key(self.identity, u32::from(prekey_id)); 507 + self.store.ks.remove(key).map_err(into_protocol_err) 508 + } 509 + } 510 + 511 + #[async_trait(?Send)] 512 + impl PreKeysStore for FjallProtocolStore { 513 + async fn next_pre_key_id(&self) -> Result<u32, SignalProtocolError> { 514 + next_id_from_max(max_id_in_prefix( 515 + &self.store.ks, 516 + &pre_key_prefix(self.identity), 517 + )?) 518 + } 519 + 520 + async fn next_signed_pre_key_id(&self) -> Result<u32, SignalProtocolError> { 521 + next_id_from_max(max_id_in_prefix( 522 + &self.store.ks, 523 + &signed_pre_key_prefix(self.identity), 524 + )?) 525 + } 526 + 527 + async fn next_pq_pre_key_id(&self) -> Result<u32, SignalProtocolError> { 528 + next_id_from_max(max_id_in_prefix( 529 + &self.store.ks, 530 + &kyber_prefix(self.identity), 531 + )?) 532 + } 533 + 534 + async fn signed_pre_keys_count(&self) -> Result<usize, SignalProtocolError> { 535 + let prefix = signed_pre_key_prefix(self.identity); 536 + let mut count = 0usize; 537 + self.store.ks.prefix(prefix).try_for_each(|guard| { 538 + guard_to_kv(guard).map_err(into_protocol_err)?; 539 + count = count.saturating_add(1); 540 + Ok::<(), SignalProtocolError>(()) 541 + })?; 542 + Ok(count) 543 + } 544 + 545 + async fn kyber_pre_keys_count(&self, last_resort: bool) -> Result<usize, SignalProtocolError> { 546 + let prefix = kyber_prefix(self.identity); 547 + let mut count = 0usize; 548 + self.store.ks.prefix(prefix).try_for_each(|guard| { 549 + let (_, val) = guard_to_kv(guard).map_err(into_protocol_err)?; 550 + if decode_kyber_value(&val).is_some_and(|(is_lr, _, _)| is_lr == last_resort) { 551 + count = count.saturating_add(1); 552 + } 553 + Ok::<(), SignalProtocolError>(()) 554 + })?; 555 + Ok(count) 556 + } 557 + 558 + async fn signed_prekey_id(&self) -> Result<Option<SignedPreKeyId>, SignalProtocolError> { 559 + max_id_in_prefix(&self.store.ks, &signed_pre_key_prefix(self.identity)) 560 + .map(|opt| opt.map(SignedPreKeyId::from)) 561 + } 562 + 563 + async fn last_resort_kyber_prekey_id( 564 + &self, 565 + ) -> Result<Option<KyberPreKeyId>, SignalProtocolError> { 566 + let prefix = kyber_prefix(self.identity); 567 + let prefix_len = prefix.len(); 568 + let mut max: Option<u32> = None; 569 + self.store.ks.prefix(prefix).try_for_each(|guard| { 570 + let (key, val) = guard_to_kv(guard).map_err(into_protocol_err)?; 571 + if let Some((true, _, _)) = decode_kyber_value(&val) 572 + && key.len() >= prefix_len + 4 573 + { 574 + let id = u32::from_be_bytes( 575 + key[prefix_len..prefix_len + 4] 576 + .try_into() 577 + .map_err(into_protocol_err)?, 578 + ); 579 + max = Some(max.map_or(id, |m| m.max(id))); 580 + } 581 + Ok::<(), SignalProtocolError>(()) 582 + })?; 583 + Ok(max.map(KyberPreKeyId::from)) 584 + } 585 + } 586 + 587 + #[async_trait(?Send)] 588 + impl SignedPreKeyStore for FjallProtocolStore { 589 + async fn get_signed_pre_key( 590 + &self, 591 + signed_prekey_id: SignedPreKeyId, 592 + ) -> Result<SignedPreKeyRecord, SignalProtocolError> { 593 + let key = signed_pre_key_key(self.identity, u32::from(signed_prekey_id)); 594 + let data = self 595 + .store 596 + .ks 597 + .get(key) 598 + .map_err(into_protocol_err)? 599 + .ok_or(SignalProtocolError::InvalidSignedPreKeyId)?; 600 + SignedPreKeyRecord::deserialize(&data) 601 + } 602 + 603 + async fn save_signed_pre_key( 604 + &mut self, 605 + signed_prekey_id: SignedPreKeyId, 606 + record: &SignedPreKeyRecord, 607 + ) -> Result<(), SignalProtocolError> { 608 + let key = signed_pre_key_key(self.identity, u32::from(signed_prekey_id)); 609 + let serialized = record.serialize()?; 610 + self.store 611 + .ks 612 + .insert(key, serialized.as_slice()) 613 + .map_err(into_protocol_err) 614 + } 615 + } 616 + 617 + #[async_trait(?Send)] 618 + impl KyberPreKeyStore for FjallProtocolStore { 619 + async fn get_kyber_pre_key( 620 + &self, 621 + kyber_prekey_id: KyberPreKeyId, 622 + ) -> Result<KyberPreKeyRecord, SignalProtocolError> { 623 + let key = kyber_key(self.identity, u32::from(kyber_prekey_id)); 624 + let data = self 625 + .store 626 + .ks 627 + .get(key) 628 + .map_err(into_protocol_err)? 629 + .ok_or(SignalProtocolError::InvalidKyberPreKeyId)?; 630 + let (_, _, record) = decode_kyber_value(&data).ok_or_else(|| { 631 + SignalProtocolError::InvalidState("kyber", "corrupted kyber pre key record".into()) 632 + })?; 633 + KyberPreKeyRecord::deserialize(record) 634 + } 635 + 636 + async fn save_kyber_pre_key( 637 + &mut self, 638 + kyber_prekey_id: KyberPreKeyId, 639 + record: &KyberPreKeyRecord, 640 + ) -> Result<(), SignalProtocolError> { 641 + let key = kyber_key(self.identity, u32::from(kyber_prekey_id)); 642 + let serialized = record.serialize()?; 643 + let value = encode_kyber_value(&serialized, false, None); 644 + self.store.ks.insert(key, &value).map_err(into_protocol_err) 645 + } 646 + 647 + async fn mark_kyber_pre_key_used( 648 + &mut self, 649 + kyber_prekey_id: KyberPreKeyId, 650 + ec_prekey_id: SignedPreKeyId, 651 + base_key: &PublicKey, 652 + ) -> Result<(), SignalProtocolError> { 653 + let key = kyber_key(self.identity, u32::from(kyber_prekey_id)); 654 + let data = self 655 + .store 656 + .ks 657 + .get(&key) 658 + .map_err(into_protocol_err)? 659 + .ok_or(SignalProtocolError::InvalidKyberPreKeyId)?; 660 + let (is_last_resort, _, _) = decode_kyber_value(&data).ok_or_else(|| { 661 + SignalProtocolError::InvalidState("kyber", "corrupted kyber pre key record".into()) 662 + })?; 663 + 664 + if is_last_resort { 665 + let base_key_bytes = base_key.serialize(); 666 + let seen_key = base_key_seen_key( 667 + self.identity, 668 + u32::from(kyber_prekey_id), 669 + u32::from(ec_prekey_id), 670 + base_key_bytes.as_ref(), 671 + ); 672 + if self 673 + .store 674 + .ks 675 + .get(&seen_key) 676 + .map_err(into_protocol_err)? 677 + .is_some() 678 + { 679 + return Err(SignalProtocolError::InvalidMessage( 680 + CiphertextMessageType::PreKey, 681 + "reused base key", 682 + )); 683 + } 684 + self.store 685 + .ks 686 + .insert(seen_key, []) 687 + .map_err(into_protocol_err)?; 688 + } else { 689 + self.store.ks.remove(key).map_err(into_protocol_err)?; 690 + } 691 + Ok(()) 692 + } 693 + } 694 + 695 + #[async_trait(?Send)] 696 + impl KyberPreKeyStoreExt for FjallProtocolStore { 697 + async fn store_last_resort_kyber_pre_key( 698 + &mut self, 699 + kyber_prekey_id: KyberPreKeyId, 700 + record: &KyberPreKeyRecord, 701 + ) -> Result<(), SignalProtocolError> { 702 + let key = kyber_key(self.identity, u32::from(kyber_prekey_id)); 703 + let serialized = record.serialize()?; 704 + let value = encode_kyber_value(&serialized, true, None); 705 + self.store.ks.insert(key, &value).map_err(into_protocol_err) 706 + } 707 + 708 + async fn load_last_resort_kyber_pre_keys( 709 + &self, 710 + ) -> Result<Vec<KyberPreKeyRecord>, SignalProtocolError> { 711 + let prefix = kyber_prefix(self.identity); 712 + let mut result = Vec::new(); 713 + self.store.ks.prefix(prefix).try_for_each(|guard| { 714 + let (_, val) = guard_to_kv(guard).map_err(into_protocol_err)?; 715 + if let Some((true, _, record)) = decode_kyber_value(&val) { 716 + result.push(KyberPreKeyRecord::deserialize(record)?); 717 + } 718 + Ok::<(), SignalProtocolError>(()) 719 + })?; 720 + Ok(result) 721 + } 722 + 723 + async fn remove_kyber_pre_key( 724 + &mut self, 725 + kyber_prekey_id: KyberPreKeyId, 726 + ) -> Result<(), SignalProtocolError> { 727 + let key = kyber_key(self.identity, u32::from(kyber_prekey_id)); 728 + self.store.ks.remove(key).map_err(into_protocol_err) 729 + } 730 + 731 + async fn mark_all_one_time_kyber_pre_keys_stale_if_necessary( 732 + &mut self, 733 + stale_time: chrono::DateTime<chrono::Utc>, 734 + ) -> Result<(), SignalProtocolError> { 735 + let stale_ms = stale_time.timestamp_millis(); 736 + let prefix = kyber_prefix(self.identity); 737 + let mut batch = self.store.db.batch(); 738 + self.store.ks.prefix(&prefix).try_for_each(|guard| { 739 + let (key, val) = guard_to_kv(guard).map_err(into_protocol_err)?; 740 + if let Some((false, None, record)) = decode_kyber_value(&val) { 741 + let new_val = encode_kyber_value(record, false, Some(stale_ms)); 742 + batch.insert(&self.store.ks, key.as_ref(), &new_val); 743 + } 744 + Ok::<(), SignalProtocolError>(()) 745 + })?; 746 + batch.commit().map_err(into_protocol_err) 747 + } 748 + 749 + async fn delete_all_stale_one_time_kyber_pre_keys( 750 + &mut self, 751 + threshold: chrono::DateTime<chrono::Utc>, 752 + min_count: usize, 753 + ) -> Result<(), SignalProtocolError> { 754 + let threshold_ms = threshold.timestamp_millis(); 755 + let prefix = kyber_prefix(self.identity); 756 + 757 + let mut total_one_time = 0usize; 758 + self.store.ks.prefix(&prefix).try_for_each(|guard| { 759 + let (_, val) = guard_to_kv(guard).map_err(into_protocol_err)?; 760 + if decode_kyber_value(&val).is_some_and(|(is_lr, _, _)| !is_lr) { 761 + total_one_time = total_one_time.saturating_add(1); 762 + } 763 + Ok::<(), SignalProtocolError>(()) 764 + })?; 765 + 766 + if total_one_time <= min_count { 767 + return Ok(()); 768 + } 769 + 770 + let mut batch = self.store.db.batch(); 771 + self.store.ks.prefix(&prefix).try_for_each(|guard| { 772 + let (key, val) = guard_to_kv(guard).map_err(into_protocol_err)?; 773 + if let Some((false, Some(stale_at), _)) = decode_kyber_value(&val) 774 + && stale_at < threshold_ms 775 + { 776 + batch.remove(&self.store.ks, key.as_ref()); 777 + } 778 + Ok::<(), SignalProtocolError>(()) 779 + })?; 780 + batch.commit().map_err(into_protocol_err) 781 + } 782 + } 783 + 784 + #[async_trait(?Send)] 785 + impl IdentityKeyStore for FjallProtocolStore { 786 + async fn get_identity_key_pair(&self) -> Result<IdentityKeyPair, SignalProtocolError> { 787 + let key_name = match self.identity { 788 + IdentityType::Aci => b"identity_keypair_aci".as_slice(), 789 + IdentityType::Pni => b"identity_keypair_pni".as_slice(), 790 + }; 791 + let bytes = self 792 + .store 793 + .ks 794 + .get(kv_key(key_name)) 795 + .map_err(into_protocol_err)? 796 + .ok_or_else(|| { 797 + SignalProtocolError::InvalidState("identity key pair", "not found in store".into()) 798 + })?; 799 + IdentityKeyPair::try_from(&*bytes) 800 + } 801 + 802 + async fn get_local_registration_id(&self) -> Result<u32, SignalProtocolError> { 803 + let data = self 804 + .store 805 + .load_registration_data() 806 + .await 807 + .map_err(into_protocol_err)? 808 + .ok_or_else(|| { 809 + SignalProtocolError::InvalidState( 810 + "failed to load registration ID", 811 + "no registration data".into(), 812 + ) 813 + })?; 814 + Ok(data.registration_id) 815 + } 816 + 817 + async fn save_identity( 818 + &mut self, 819 + address: &ProtocolAddress, 820 + identity_key_val: &IdentityKey, 821 + ) -> Result<IdentityChange, SignalProtocolError> { 822 + let existing = self.get_identity(address).await?; 823 + 824 + let key = identity_rec_key(self.identity, address.name()); 825 + let serialized = identity_key_val.serialize(); 826 + self.store 827 + .ks 828 + .insert(key, &*serialized) 829 + .map_err(into_protocol_err)?; 830 + 831 + Ok(match existing { 832 + Some(k) if k == *identity_key_val => IdentityChange::NewOrUnchanged, 833 + Some(_) => IdentityChange::ReplacedExisting, 834 + None => IdentityChange::NewOrUnchanged, 835 + }) 836 + } 837 + 838 + async fn is_trusted_identity( 839 + &self, 840 + address: &ProtocolAddress, 841 + identity_key_val: &IdentityKey, 842 + _direction: Direction, 843 + ) -> Result<bool, SignalProtocolError> { 844 + match self.get_identity(address).await? { 845 + Some(trusted_key) if identity_key_val == &trusted_key => Ok(true), 846 + Some(_) => { 847 + warn!(%address, "trusting changed identity"); 848 + Ok(true) 849 + } 850 + None => { 851 + warn!(%address, "trusting new identity"); 852 + Ok(true) 853 + } 854 + } 855 + } 856 + 857 + async fn get_identity( 858 + &self, 859 + address: &ProtocolAddress, 860 + ) -> Result<Option<IdentityKey>, SignalProtocolError> { 861 + let key = identity_rec_key(self.identity, address.name()); 862 + self.store 863 + .ks 864 + .get(key) 865 + .map_err(into_protocol_err)? 866 + .map(|bytes| IdentityKey::decode(&bytes)) 867 + .transpose() 868 + } 869 + } 870 + 871 + #[async_trait(?Send)] 872 + impl SenderKeyStore for FjallProtocolStore { 873 + async fn store_sender_key( 874 + &mut self, 875 + sender: &ProtocolAddress, 876 + distribution_id: Uuid, 877 + record: &SenderKeyRecord, 878 + ) -> Result<(), SignalProtocolError> { 879 + let key = sender_key_key( 880 + self.identity, 881 + sender.name(), 882 + u32::from(sender.device_id()), 883 + distribution_id, 884 + ); 885 + let serialized = record.serialize()?; 886 + self.store 887 + .ks 888 + .insert(key, serialized.as_slice()) 889 + .map_err(into_protocol_err) 890 + } 891 + 892 + async fn load_sender_key( 893 + &mut self, 894 + sender: &ProtocolAddress, 895 + distribution_id: Uuid, 896 + ) -> Result<Option<SenderKeyRecord>, SignalProtocolError> { 897 + let key = sender_key_key( 898 + self.identity, 899 + sender.name(), 900 + u32::from(sender.device_id()), 901 + distribution_id, 902 + ); 903 + self.store 904 + .ks 905 + .get(key) 906 + .map_err(into_protocol_err)? 907 + .map(|record| SenderKeyRecord::deserialize(&record)) 908 + .transpose() 909 + } 910 + } 911 + 912 + type EmptyIter<T> = std::iter::Empty<Result<T, FjallStoreError>>; 913 + 914 + impl ContentsStore for FjallSignalStore { 915 + type ContentsStoreError = FjallStoreError; 916 + type ContactsIter = EmptyIter<Contact>; 917 + type GroupsIter = EmptyIter<(GroupMasterKeyBytes, Group)>; 918 + type MessagesIter = EmptyIter<Content>; 919 + type StickerPacksIter = EmptyIter<StickerPack>; 920 + 921 + async fn clear_profiles(&mut self) -> Result<(), FjallStoreError> { 922 + let mut batch = self.db.batch(); 923 + self.ks.prefix([b'R']).try_for_each(|guard| { 924 + let (key, _) = guard.into_inner()?; 925 + batch.remove(&self.ks, key.as_ref()); 926 + Ok::<(), fjall::Error>(()) 927 + })?; 928 + batch.commit()?; 929 + Ok(()) 930 + } 931 + 932 + async fn clear_contents(&mut self) -> Result<(), FjallStoreError> { 933 + Ok(()) 934 + } 935 + 936 + async fn clear_messages(&mut self) -> Result<(), FjallStoreError> { 937 + Ok(()) 938 + } 939 + 940 + async fn clear_thread(&mut self, _thread: &Thread) -> Result<(), FjallStoreError> { 941 + Ok(()) 942 + } 943 + 944 + async fn save_message( 945 + &self, 946 + _thread: &Thread, 947 + _message: Content, 948 + ) -> Result<(), FjallStoreError> { 949 + Ok(()) 950 + } 951 + 952 + async fn delete_message( 953 + &mut self, 954 + _thread: &Thread, 955 + _timestamp: u64, 956 + ) -> Result<bool, FjallStoreError> { 957 + Ok(false) 958 + } 959 + 960 + async fn message( 961 + &self, 962 + _thread: &Thread, 963 + _timestamp: u64, 964 + ) -> Result<Option<Content>, FjallStoreError> { 965 + Ok(None) 966 + } 967 + 968 + async fn messages( 969 + &self, 970 + _thread: &Thread, 971 + _range: impl RangeBounds<u64>, 972 + ) -> Result<Self::MessagesIter, FjallStoreError> { 973 + Ok(std::iter::empty()) 974 + } 975 + 976 + async fn clear_contacts(&mut self) -> Result<(), FjallStoreError> { 977 + Ok(()) 978 + } 979 + 980 + async fn save_contact(&mut self, _contact: &Contact) -> Result<(), FjallStoreError> { 981 + Ok(()) 982 + } 983 + 984 + async fn contacts(&self) -> Result<Self::ContactsIter, FjallStoreError> { 985 + Ok(std::iter::empty()) 986 + } 987 + 988 + async fn contact_by_id(&self, _id: &ServiceId) -> Result<Option<Contact>, FjallStoreError> { 989 + Ok(None) 990 + } 991 + 992 + async fn clear_groups(&mut self) -> Result<(), FjallStoreError> { 993 + Ok(()) 994 + } 995 + 996 + async fn save_group( 997 + &self, 998 + _master_key: GroupMasterKeyBytes, 999 + _group: impl Into<Group>, 1000 + ) -> Result<(), FjallStoreError> { 1001 + Ok(()) 1002 + } 1003 + 1004 + async fn groups(&self) -> Result<Self::GroupsIter, FjallStoreError> { 1005 + Ok(std::iter::empty()) 1006 + } 1007 + 1008 + async fn group( 1009 + &self, 1010 + _master_key: GroupMasterKeyBytes, 1011 + ) -> Result<Option<Group>, FjallStoreError> { 1012 + Ok(None) 1013 + } 1014 + 1015 + async fn save_group_avatar( 1016 + &self, 1017 + _master_key: GroupMasterKeyBytes, 1018 + _avatar: &AvatarBytes, 1019 + ) -> Result<(), FjallStoreError> { 1020 + Ok(()) 1021 + } 1022 + 1023 + async fn group_avatar( 1024 + &self, 1025 + _master_key: GroupMasterKeyBytes, 1026 + ) -> Result<Option<AvatarBytes>, FjallStoreError> { 1027 + Ok(None) 1028 + } 1029 + 1030 + async fn upsert_profile_key( 1031 + &mut self, 1032 + uuid: &Uuid, 1033 + key: ProfileKey, 1034 + ) -> Result<bool, FjallStoreError> { 1035 + let k = profile_key_key(uuid); 1036 + let existed = self.ks.get(&k)?.is_some(); 1037 + self.ks.insert(k, key.bytes)?; 1038 + Ok(!existed) 1039 + } 1040 + 1041 + async fn profile_key( 1042 + &self, 1043 + service_id: &ServiceId, 1044 + ) -> Result<Option<ProfileKey>, FjallStoreError> { 1045 + let uuid = service_id.raw_uuid(); 1046 + let k = profile_key_key(&uuid); 1047 + Ok(self 1048 + .ks 1049 + .get(k)? 1050 + .and_then(|v| match <[u8; 32]>::try_from(v.as_ref()) { 1051 + Ok(arr) => Some(ProfileKey { bytes: arr }), 1052 + Err(_) => { 1053 + warn!(%uuid, len = v.len(), "corrupted profile key (expected 32 bytes)"); 1054 + None 1055 + } 1056 + })) 1057 + } 1058 + 1059 + async fn save_profile( 1060 + &mut self, 1061 + _uuid: Uuid, 1062 + _key: ProfileKey, 1063 + _profile: Profile, 1064 + ) -> Result<(), FjallStoreError> { 1065 + Ok(()) 1066 + } 1067 + 1068 + async fn profile( 1069 + &self, 1070 + _uuid: Uuid, 1071 + _key: ProfileKey, 1072 + ) -> Result<Option<Profile>, FjallStoreError> { 1073 + Ok(None) 1074 + } 1075 + 1076 + async fn save_profile_avatar( 1077 + &mut self, 1078 + _uuid: Uuid, 1079 + _key: ProfileKey, 1080 + _profile: &AvatarBytes, 1081 + ) -> Result<(), FjallStoreError> { 1082 + Ok(()) 1083 + } 1084 + 1085 + async fn profile_avatar( 1086 + &self, 1087 + _uuid: Uuid, 1088 + _key: ProfileKey, 1089 + ) -> Result<Option<AvatarBytes>, FjallStoreError> { 1090 + Ok(None) 1091 + } 1092 + 1093 + async fn add_sticker_pack(&mut self, _pack: &StickerPack) -> Result<(), FjallStoreError> { 1094 + Ok(()) 1095 + } 1096 + 1097 + async fn sticker_pack(&self, _id: &[u8]) -> Result<Option<StickerPack>, FjallStoreError> { 1098 + Ok(None) 1099 + } 1100 + 1101 + async fn remove_sticker_pack(&mut self, _id: &[u8]) -> Result<bool, FjallStoreError> { 1102 + Ok(false) 1103 + } 1104 + 1105 + async fn sticker_packs(&self) -> Result<Self::StickerPacksIter, FjallStoreError> { 1106 + Ok(std::iter::empty()) 1107 + } 1108 + } 1109 + 1110 + pub struct FjallSignalStoreProvider { 1111 + store: FjallSignalStore, 1112 + } 1113 + 1114 + impl FjallSignalStoreProvider { 1115 + pub fn new(db: Database, ks: Keyspace) -> Self { 1116 + Self { 1117 + store: FjallSignalStore::new(db, ks), 1118 + } 1119 + } 1120 + } 1121 + 1122 + #[async_trait::async_trait] 1123 + impl crate::SignalStoreProvider for FjallSignalStoreProvider { 1124 + async fn is_signal_linked(&self) -> bool { 1125 + self.store.is_linked().unwrap_or(false) 1126 + } 1127 + 1128 + async fn clear_signal_data(&self) -> Result<(), SignalError> { 1129 + self.store 1130 + .clear_all() 1131 + .map_err(|e| SignalError::Store(e.to_string())) 1132 + } 1133 + 1134 + async fn link_signal_device( 1135 + &self, 1136 + device_name: DeviceName, 1137 + shutdown: tokio_util::sync::CancellationToken, 1138 + link_cancel: tokio_util::sync::CancellationToken, 1139 + linking_flag: Arc<std::sync::atomic::AtomicBool>, 1140 + ) -> Result<LinkResult, SignalError> { 1141 + SignalClient::link_device_with_store( 1142 + self.store.clone(), 1143 + device_name, 1144 + shutdown, 1145 + link_cancel, 1146 + linking_flag, 1147 + ) 1148 + .await 1149 + } 1150 + 1151 + async fn load_signal_client( 1152 + &self, 1153 + shutdown: tokio_util::sync::CancellationToken, 1154 + ) -> Option<SignalClient> { 1155 + SignalClient::from_store(self.store.clone(), shutdown).await 1156 + } 1157 + }
+59
crates/tranquil-signal/src/lib.rs
··· 1 1 mod client; 2 2 pub mod store; 3 3 4 + #[cfg(feature = "fjall-store")] 5 + pub mod fjall_store; 6 + 4 7 #[cfg(test)] 5 8 mod tests; 6 9 ··· 10 13 }; 11 14 pub use presage; 12 15 pub use store::PgSignalStore; 16 + 17 + #[async_trait::async_trait] 18 + pub trait SignalStoreProvider: Send + Sync { 19 + async fn is_signal_linked(&self) -> bool; 20 + async fn clear_signal_data(&self) -> Result<(), SignalError>; 21 + async fn link_signal_device( 22 + &self, 23 + device_name: DeviceName, 24 + shutdown: tokio_util::sync::CancellationToken, 25 + link_cancel: tokio_util::sync::CancellationToken, 26 + linking_flag: std::sync::Arc<std::sync::atomic::AtomicBool>, 27 + ) -> Result<LinkResult, SignalError>; 28 + async fn load_signal_client( 29 + &self, 30 + shutdown: tokio_util::sync::CancellationToken, 31 + ) -> Option<SignalClient>; 32 + } 33 + 34 + pub struct PgSignalStoreProvider { 35 + pub pool: sqlx::PgPool, 36 + } 37 + 38 + #[async_trait::async_trait] 39 + impl SignalStoreProvider for PgSignalStoreProvider { 40 + async fn is_signal_linked(&self) -> bool { 41 + PgSignalStore::new(self.pool.clone()) 42 + .is_linked() 43 + .await 44 + .unwrap_or(false) 45 + } 46 + 47 + async fn clear_signal_data(&self) -> Result<(), SignalError> { 48 + PgSignalStore::new(self.pool.clone()) 49 + .clear_all() 50 + .await 51 + .map_err(SignalError::from) 52 + } 53 + 54 + async fn link_signal_device( 55 + &self, 56 + device_name: DeviceName, 57 + shutdown: tokio_util::sync::CancellationToken, 58 + link_cancel: tokio_util::sync::CancellationToken, 59 + linking_flag: std::sync::Arc<std::sync::atomic::AtomicBool>, 60 + ) -> Result<LinkResult, SignalError> { 61 + SignalClient::link_device(&self.pool, device_name, shutdown, link_cancel, linking_flag) 62 + .await 63 + } 64 + 65 + async fn load_signal_client( 66 + &self, 67 + shutdown: tokio_util::sync::CancellationToken, 68 + ) -> Option<SignalClient> { 69 + SignalClient::from_pool(&self.pool, shutdown).await 70 + } 71 + }
+2
crates/tranquil-store/src/lib.rs
··· 1 1 pub mod blockstore; 2 2 pub mod eventlog; 3 3 pub mod fsync_order; 4 + #[cfg(any(test, feature = "test-harness"))] 4 5 mod harness; 5 6 mod io; 6 7 pub mod metastore; ··· 19 20 FILE_MAGIC, FORMAT_VERSION, HEADER_SIZE, MAX_RECORD_PAYLOAD, RECORD_OVERHEAD, ReadRecord, 20 21 RecordReader, RecordWriter, 21 22 }; 23 + #[cfg(any(test, feature = "test-harness"))] 22 24 pub use sim::{FaultConfig, OpRecord, SimulatedIO};
+189 -6
crates/tranquil-store/src/metastore/client.rs
··· 9 9 ApplyCommitResult, Backlink, BrokenGenesisCommit, CommitEventData, CommsChannel, CommsType, 10 10 CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput, 11 11 CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult, 12 - CreateSsoAccountInput, DbError, DeletionRequest, DidWebOverrides, EventBlocksCids, ImportBlock, 13 - ImportRecord, ImportRepoError, InviteCodeError, InviteCodeInfo, InviteCodeRow, 14 - InviteCodeSortOrder, InviteCodeUse, MigrationReactivationError, MigrationReactivationInput, 15 - NotificationHistoryRow, NotificationPrefs, OAuthTokenWithUser, PasswordResetResult, 16 - QueuedComms, ReactivatedAccountInfo, RecoverPasskeyAccountInput, RecoverPasskeyAccountResult, 17 - RepoAccountInfo, RepoInfo, RepoListItem, RepoWithoutRev, ReservedSigningKey, 12 + CreateSsoAccountInput, DbError, DeletionRequest, DeletionRequestWithToken, DidWebOverrides, 13 + EventBlocksCids, ImportBlock, ImportRecord, ImportRepoError, InviteCodeError, InviteCodeInfo, 14 + InviteCodeRow, InviteCodeSortOrder, InviteCodeUse, MigrationReactivationError, 15 + MigrationReactivationInput, NotificationHistoryRow, NotificationPrefs, OAuthTokenWithUser, 16 + PasswordResetResult, PlcTokenInfo, QueuedComms, ReactivatedAccountInfo, 17 + RecoverPasskeyAccountInput, RecoverPasskeyAccountResult, RepoAccountInfo, RepoInfo, 18 + RepoListItem, RepoWithoutRev, ReservedSigningKey, ReservedSigningKeyFull, 18 19 ScheduledDeletionAccount, ScopePreference, SequenceNumber, SequencedEvent, StoredBackupCode, 19 20 StoredPasskey, TokenFamilyId, TotpRecord, TotpRecordState, User2faStatus, UserAuthInfo, 20 21 UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, UserForDeletion, ··· 2459 2460 ))?; 2460 2461 recv(rx).await 2461 2462 } 2463 + 2464 + async fn get_deletion_request_by_did( 2465 + &self, 2466 + did: &Did, 2467 + ) -> Result<Option<DeletionRequestWithToken>, DbError> { 2468 + let (tx, rx) = oneshot::channel(); 2469 + self.pool.send(MetastoreRequest::Infra( 2470 + InfraRequest::GetDeletionRequestByDid { 2471 + did: did.clone(), 2472 + tx, 2473 + }, 2474 + ))?; 2475 + recv(rx).await 2476 + } 2477 + 2478 + async fn get_latest_comms_for_user( 2479 + &self, 2480 + user_id: Uuid, 2481 + comms_type: CommsType, 2482 + limit: i64, 2483 + ) -> Result<Vec<QueuedComms>, DbError> { 2484 + let (tx, rx) = oneshot::channel(); 2485 + self.pool.send(MetastoreRequest::Infra( 2486 + InfraRequest::GetLatestCommsForUser { 2487 + user_id, 2488 + comms_type, 2489 + limit, 2490 + tx, 2491 + }, 2492 + ))?; 2493 + recv(rx).await 2494 + } 2495 + 2496 + async fn count_comms_by_type( 2497 + &self, 2498 + user_id: Uuid, 2499 + comms_type: CommsType, 2500 + ) -> Result<i64, DbError> { 2501 + let (tx, rx) = oneshot::channel(); 2502 + self.pool 2503 + .send(MetastoreRequest::Infra(InfraRequest::CountCommsByType { 2504 + user_id, 2505 + comms_type, 2506 + tx, 2507 + }))?; 2508 + recv(rx).await 2509 + } 2510 + 2511 + async fn delete_comms_by_type_for_user( 2512 + &self, 2513 + user_id: Uuid, 2514 + comms_type: CommsType, 2515 + ) -> Result<u64, DbError> { 2516 + let (tx, rx) = oneshot::channel(); 2517 + self.pool.send(MetastoreRequest::Infra( 2518 + InfraRequest::DeleteCommsByTypeForUser { 2519 + user_id, 2520 + comms_type, 2521 + tx, 2522 + }, 2523 + ))?; 2524 + recv(rx).await 2525 + } 2526 + 2527 + async fn expire_deletion_request(&self, token: &str) -> Result<(), DbError> { 2528 + let (tx, rx) = oneshot::channel(); 2529 + self.pool.send(MetastoreRequest::Infra( 2530 + InfraRequest::ExpireDeletionRequest { 2531 + token: token.to_owned(), 2532 + tx, 2533 + }, 2534 + ))?; 2535 + recv(rx).await 2536 + } 2537 + 2538 + async fn get_reserved_signing_key_full( 2539 + &self, 2540 + public_key_did_key: &str, 2541 + ) -> Result<Option<ReservedSigningKeyFull>, DbError> { 2542 + let (tx, rx) = oneshot::channel(); 2543 + self.pool.send(MetastoreRequest::Infra( 2544 + InfraRequest::GetReservedSigningKeyFull { 2545 + public_key_did_key: public_key_did_key.to_owned(), 2546 + tx, 2547 + }, 2548 + ))?; 2549 + recv(rx).await 2550 + } 2551 + 2552 + async fn get_plc_tokens_by_did(&self, did: &Did) -> Result<Vec<PlcTokenInfo>, DbError> { 2553 + let (tx, rx) = oneshot::channel(); 2554 + self.pool 2555 + .send(MetastoreRequest::Infra(InfraRequest::GetPlcTokensByDid { 2556 + did: did.clone(), 2557 + tx, 2558 + }))?; 2559 + recv(rx).await 2560 + } 2561 + 2562 + async fn count_plc_tokens_by_did(&self, did: &Did) -> Result<i64, DbError> { 2563 + let (tx, rx) = oneshot::channel(); 2564 + self.pool 2565 + .send(MetastoreRequest::Infra(InfraRequest::CountPlcTokensByDid { 2566 + did: did.clone(), 2567 + tx, 2568 + }))?; 2569 + recv(rx).await 2570 + } 2462 2571 } 2463 2572 2464 2573 #[async_trait] ··· 3227 3336 ))?; 3228 3337 recv(rx).await 3229 3338 } 3339 + 3340 + async fn get_2fa_challenge_code( 3341 + &self, 3342 + request_uri: &RequestId, 3343 + ) -> Result<Option<String>, DbError> { 3344 + let (tx, rx) = oneshot::channel(); 3345 + self.pool 3346 + .send(MetastoreRequest::OAuth(OAuthRequest::Get2faChallengeCode { 3347 + request_uri: request_uri.clone(), 3348 + tx, 3349 + }))?; 3350 + recv(rx).await 3351 + } 3230 3352 } 3231 3353 3232 3354 #[async_trait] ··· 3685 3807 .send(MetastoreRequest::User(UserRequest::AdminUpdatePassword { 3686 3808 did: did.clone(), 3687 3809 password_hash: password_hash.to_owned(), 3810 + tx, 3811 + }))?; 3812 + recv(rx).await 3813 + } 3814 + 3815 + async fn set_admin_status(&self, did: &Did, is_admin: bool) -> Result<(), DbError> { 3816 + let (tx, rx) = oneshot::channel(); 3817 + self.pool 3818 + .send(MetastoreRequest::User(UserRequest::SetAdminStatus { 3819 + did: did.clone(), 3820 + is_admin, 3688 3821 tx, 3689 3822 }))?; 3690 3823 recv(rx).await ··· 4887 5020 input: input.clone(), 4888 5021 tx, 4889 5022 }))?; 5023 + recv(rx).await 5024 + } 5025 + 5026 + async fn get_password_reset_info( 5027 + &self, 5028 + email: &str, 5029 + ) -> Result<Option<tranquil_db_traits::PasswordResetInfo>, DbError> { 5030 + let (tx, rx) = oneshot::channel(); 5031 + self.pool 5032 + .send(MetastoreRequest::User(UserRequest::GetPasswordResetInfo { 5033 + email: email.to_owned(), 5034 + tx, 5035 + }))?; 5036 + recv(rx).await 5037 + } 5038 + 5039 + async fn enable_totp_verified( 5040 + &self, 5041 + did: &Did, 5042 + encrypted_secret: &[u8], 5043 + ) -> Result<(), DbError> { 5044 + let (tx, rx) = oneshot::channel(); 5045 + self.pool 5046 + .send(MetastoreRequest::User(UserRequest::EnableTotpVerified { 5047 + did: did.clone(), 5048 + encrypted_secret: encrypted_secret.to_vec(), 5049 + tx, 5050 + }))?; 5051 + recv(rx).await 5052 + } 5053 + 5054 + async fn set_two_factor_enabled(&self, did: &Did, enabled: bool) -> Result<(), DbError> { 5055 + let (tx, rx) = oneshot::channel(); 5056 + self.pool 5057 + .send(MetastoreRequest::User(UserRequest::SetTwoFactorEnabled { 5058 + did: did.clone(), 5059 + enabled, 5060 + tx, 5061 + }))?; 5062 + recv(rx).await 5063 + } 5064 + 5065 + async fn expire_password_reset_code(&self, email: &str) -> Result<(), DbError> { 5066 + let (tx, rx) = oneshot::channel(); 5067 + self.pool.send(MetastoreRequest::User( 5068 + UserRequest::ExpirePasswordResetCode { 5069 + email: email.to_owned(), 5070 + tx, 5071 + }, 5072 + ))?; 4890 5073 recv(rx).await 4891 5074 } 4892 5075 }
+272 -26
crates/tranquil-store/src/metastore/handler.rs
··· 10 10 ApplyCommitResult, Backlink, BrokenGenesisCommit, CommitEventData, CommsChannel, CommsType, 11 11 CompletePasskeySetupInput, CreateAccountError, CreateDelegatedAccountInput, 12 12 CreatePasskeyAccountInput, CreatePasswordAccountInput, CreatePasswordAccountResult, 13 - CreateSsoAccountInput, DbError, DelegationActionType, DeletionRequest, DidWebOverrides, 14 - EventBlocksCids, ImportBlock, ImportRecord, ImportRepoError, InviteCodeError, InviteCodeInfo, 15 - InviteCodeRow, InviteCodeSortOrder, InviteCodeUse, MigrationReactivationError, 16 - MigrationReactivationInput, NotificationHistoryRow, NotificationPrefs, OAuthTokenWithUser, 17 - PasswordResetResult, QueuedComms, ReactivatedAccountInfo, RecoverPasskeyAccountInput, 18 - RecoverPasskeyAccountResult, RefreshSessionResult, ReservedSigningKey, 19 - ScheduledDeletionAccount, ScopePreference, SequenceNumber, SequencedEvent, SessionId, 20 - StoredBackupCode, StoredPasskey, TokenFamilyId, TotpRecord, TotpRecordState, User2faStatus, 21 - UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo, 22 - UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery, 23 - UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle, 24 - UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId, 25 - UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, 26 - UserNeedingRecordBlobsBackfill, UserPasswordInfo, UserResendVerification, UserResetCodeInfo, 27 - UserRow, UserSessionInfo, UserStatus, UserVerificationInfo, UserWithKey, UserWithoutBlocks, 28 - ValidatedInviteCode, WebauthnChallengeType, 13 + CreateSsoAccountInput, DbError, DelegationActionType, DeletionRequest, 14 + DeletionRequestWithToken, DidWebOverrides, EventBlocksCids, ImportBlock, ImportRecord, 15 + ImportRepoError, InviteCodeError, InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, 16 + InviteCodeUse, MigrationReactivationError, MigrationReactivationInput, NotificationHistoryRow, 17 + NotificationPrefs, OAuthTokenWithUser, PasswordResetResult, PlcTokenInfo, QueuedComms, 18 + ReactivatedAccountInfo, RecoverPasskeyAccountInput, RecoverPasskeyAccountResult, 19 + RefreshSessionResult, ReservedSigningKey, ReservedSigningKeyFull, ScheduledDeletionAccount, 20 + ScopePreference, SequenceNumber, SequencedEvent, SessionId, StoredBackupCode, StoredPasskey, 21 + TokenFamilyId, TotpRecord, TotpRecordState, User2faStatus, UserAuthInfo, UserCommsPrefs, 22 + UserConfirmSignup, UserDidWebInfo, UserEmailInfo, UserForDeletion, UserForDidDoc, 23 + UserForDidDocBuild, UserForPasskeyRecovery, UserForPasskeySetup, UserForRecovery, 24 + UserForVerification, UserIdAndHandle, UserIdAndPasswordHash, UserIdHandleEmail, 25 + UserInfoForAuth, UserKeyInfo, UserKeyWithId, UserLegacyLoginPref, UserLoginCheck, 26 + UserLoginFull, UserLoginInfo, UserNeedingRecordBlobsBackfill, UserPasswordInfo, 27 + UserResendVerification, UserResetCodeInfo, UserRow, UserSessionInfo, UserStatus, 28 + UserVerificationInfo, UserWithKey, UserWithoutBlocks, ValidatedInviteCode, 29 + WebauthnChallengeType, 29 30 }; 30 31 use tranquil_oauth::{AuthorizedClientData, DeviceData, RequestData, TokenData}; 31 32 use tranquil_types::{ ··· 1127 1128 password_hash: String, 1128 1129 tx: Tx<u64>, 1129 1130 }, 1131 + SetAdminStatus { 1132 + did: Did, 1133 + is_admin: bool, 1134 + tx: Tx<()>, 1135 + }, 1130 1136 GetNotificationPrefs { 1131 1137 did: Did, 1132 1138 tx: Tx<Option<NotificationPrefs>>, ··· 1559 1565 input: RecoverPasskeyAccountInput, 1560 1566 tx: Tx<RecoverPasskeyAccountResult>, 1561 1567 }, 1568 + GetPasswordResetInfo { 1569 + email: String, 1570 + tx: Tx<Option<tranquil_db_traits::PasswordResetInfo>>, 1571 + }, 1572 + EnableTotpVerified { 1573 + did: Did, 1574 + encrypted_secret: Vec<u8>, 1575 + tx: Tx<()>, 1576 + }, 1577 + SetTwoFactorEnabled { 1578 + did: Did, 1579 + enabled: bool, 1580 + tx: Tx<()>, 1581 + }, 1582 + ExpirePasswordResetCode { 1583 + email: String, 1584 + tx: Tx<()>, 1585 + }, 1562 1586 } 1563 1587 1564 1588 impl UserRequest { ··· 1585 1609 | Self::AdminUpdateEmail { did, .. } 1586 1610 | Self::AdminUpdateHandle { did, .. } 1587 1611 | Self::AdminUpdatePassword { did, .. } 1612 + | Self::SetAdminStatus { did, .. } 1588 1613 | Self::GetNotificationPrefs { did, .. } 1589 1614 | Self::GetIdHandleEmailByDid { did, .. } 1590 1615 | Self::UpdatePreferredCommsChannel { did, .. } ··· 1662 1687 input: RecoverPasskeyAccountInput { did, .. }, 1663 1688 .. 1664 1689 } 1665 - | Self::SetRecoveryToken { did, .. } => did_to_routing(did.as_str()), 1690 + | Self::SetRecoveryToken { did, .. } 1691 + | Self::EnableTotpVerified { did, .. } 1692 + | Self::SetTwoFactorEnabled { did, .. } => did_to_routing(did.as_str()), 1666 1693 1667 1694 Self::GetCommsPrefs { user_id, .. } 1668 1695 | Self::GetUserKeyById { user_id, .. } ··· 1729 1756 | Self::GetUserForPasskeyRecovery { .. } 1730 1757 | Self::GetAccountsScheduledForDeletion { .. } 1731 1758 | Self::CleanupExpiredHandleReservations { .. } 1732 - | Self::CheckAndConsumeInviteCode { .. } => Routing::Global, 1759 + | Self::CheckAndConsumeInviteCode { .. } 1760 + | Self::GetPasswordResetInfo { .. } 1761 + | Self::ExpirePasswordResetCode { .. } => Routing::Global, 1733 1762 } 1734 1763 } 1735 1764 } ··· 1969 1998 user_ids: Vec<Uuid>, 1970 1999 tx: Tx<Vec<(Uuid, String)>>, 1971 2000 }, 2001 + GetDeletionRequestByDid { 2002 + did: Did, 2003 + tx: Tx<Option<DeletionRequestWithToken>>, 2004 + }, 2005 + GetLatestCommsForUser { 2006 + user_id: Uuid, 2007 + comms_type: CommsType, 2008 + limit: i64, 2009 + tx: Tx<Vec<QueuedComms>>, 2010 + }, 2011 + CountCommsByType { 2012 + user_id: Uuid, 2013 + comms_type: CommsType, 2014 + tx: Tx<i64>, 2015 + }, 2016 + DeleteCommsByTypeForUser { 2017 + user_id: Uuid, 2018 + comms_type: CommsType, 2019 + tx: Tx<u64>, 2020 + }, 2021 + ExpireDeletionRequest { 2022 + token: String, 2023 + tx: Tx<()>, 2024 + }, 2025 + GetReservedSigningKeyFull { 2026 + public_key_did_key: String, 2027 + tx: Tx<Option<ReservedSigningKeyFull>>, 2028 + }, 2029 + GetPlcTokensByDid { 2030 + did: Did, 2031 + tx: Tx<Vec<PlcTokenInfo>>, 2032 + }, 2033 + CountPlcTokensByDid { 2034 + did: Did, 2035 + tx: Tx<i64>, 2036 + }, 1972 2037 } 1973 2038 1974 2039 impl InfraRequest { ··· 1986 2051 | Self::GetInvitesCreatedByUser { user_id, .. } 1987 2052 | Self::GetInviteCodeUsedByUser { user_id, .. } 1988 2053 | Self::DeleteInviteCodeUsesByUser { user_id, .. } 1989 - | Self::DeleteInviteCodesByUser { user_id, .. } => { 2054 + | Self::DeleteInviteCodesByUser { user_id, .. } 2055 + | Self::GetLatestCommsForUser { user_id, .. } 2056 + | Self::CountCommsByType { user_id, .. } 2057 + | Self::DeleteCommsByTypeForUser { user_id, .. } => { 1990 2058 uuid_to_routing(user_hashes, user_id) 1991 2059 } 1992 2060 Self::CreateInviteCodesBatch { ··· 2003 2071 } 2004 2072 Self::DeleteDeletionRequestsByDid { did, .. } 2005 2073 | Self::CreateDeletionRequest { did, .. } 2006 - | Self::GetAdminAccountInfoByDid { did, .. } => did_to_routing(did.as_str()), 2074 + | Self::GetAdminAccountInfoByDid { did, .. } 2075 + | Self::GetDeletionRequestByDid { did, .. } 2076 + | Self::GetPlcTokensByDid { did, .. } 2077 + | Self::CountPlcTokensByDid { did, .. } => did_to_routing(did.as_str()), 2007 2078 Self::GetBlobStorageKeyByCid { cid, .. } | Self::DeleteBlobByCid { cid, .. } => { 2008 2079 cid_to_routing(cid) 2009 2080 } ··· 2279 2350 except_token_id: TokenId, 2280 2351 tx: Tx<u64>, 2281 2352 }, 2353 + Get2faChallengeCode { 2354 + request_uri: RequestId, 2355 + tx: Tx<Option<String>>, 2356 + }, 2282 2357 } 2283 2358 2284 2359 impl OAuthRequest { ··· 2342 2417 | Self::RevokeDeviceTrust { .. } 2343 2418 | Self::UpdateDeviceFriendlyName { .. } 2344 2419 | Self::TrustDevice { .. } 2345 - | Self::ExtendDeviceTrust { .. } => Routing::Global, 2420 + | Self::ExtendDeviceTrust { .. } 2421 + | Self::Get2faChallengeCode { .. } => Routing::Global, 2346 2422 } 2347 2423 } 2348 2424 } ··· 4266 4342 .map_err(metastore_to_db); 4267 4343 let _ = tx.send(result); 4268 4344 } 4345 + InfraRequest::GetDeletionRequestByDid { did, tx } => { 4346 + let result = state 4347 + .metastore 4348 + .infra_ops() 4349 + .get_deletion_request_by_did(&did) 4350 + .map_err(metastore_to_db); 4351 + let _ = tx.send(result); 4352 + } 4353 + InfraRequest::GetLatestCommsForUser { 4354 + user_id, 4355 + comms_type, 4356 + limit, 4357 + tx, 4358 + } => { 4359 + let result = state 4360 + .metastore 4361 + .infra_ops() 4362 + .get_latest_comms_for_user(user_id, comms_type, limit) 4363 + .map_err(metastore_to_db); 4364 + let _ = tx.send(result); 4365 + } 4366 + InfraRequest::CountCommsByType { 4367 + user_id, 4368 + comms_type, 4369 + tx, 4370 + } => { 4371 + let result = state 4372 + .metastore 4373 + .infra_ops() 4374 + .count_comms_by_type(user_id, comms_type) 4375 + .map_err(metastore_to_db); 4376 + let _ = tx.send(result); 4377 + } 4378 + InfraRequest::DeleteCommsByTypeForUser { 4379 + user_id, 4380 + comms_type, 4381 + tx, 4382 + } => { 4383 + let result = state 4384 + .metastore 4385 + .infra_ops() 4386 + .delete_comms_by_type_for_user(user_id, comms_type) 4387 + .map_err(metastore_to_db); 4388 + let _ = tx.send(result); 4389 + } 4390 + InfraRequest::ExpireDeletionRequest { token, tx } => { 4391 + let result = state 4392 + .metastore 4393 + .infra_ops() 4394 + .expire_deletion_request(&token) 4395 + .map_err(metastore_to_db); 4396 + let _ = tx.send(result); 4397 + } 4398 + InfraRequest::GetReservedSigningKeyFull { 4399 + public_key_did_key, 4400 + tx, 4401 + } => { 4402 + let result = state 4403 + .metastore 4404 + .infra_ops() 4405 + .get_reserved_signing_key_full(&public_key_did_key) 4406 + .map_err(metastore_to_db); 4407 + let _ = tx.send(result); 4408 + } 4409 + InfraRequest::GetPlcTokensByDid { did, tx } => { 4410 + let result = (|| { 4411 + let user_id = state 4412 + .metastore 4413 + .user_ops() 4414 + .get_id_by_did(&did) 4415 + .map_err(metastore_to_db)?; 4416 + match user_id { 4417 + Some(uid) => state 4418 + .metastore 4419 + .infra_ops() 4420 + .get_plc_tokens_for_user(uid) 4421 + .map_err(metastore_to_db), 4422 + None => Ok(Vec::new()), 4423 + } 4424 + })(); 4425 + let _ = tx.send(result); 4426 + } 4427 + InfraRequest::CountPlcTokensByDid { did, tx } => { 4428 + let result = (|| { 4429 + let user_id = state 4430 + .metastore 4431 + .user_ops() 4432 + .get_id_by_did(&did) 4433 + .map_err(metastore_to_db)?; 4434 + match user_id { 4435 + Some(uid) => state 4436 + .metastore 4437 + .infra_ops() 4438 + .count_plc_tokens_for_user(uid) 4439 + .map_err(metastore_to_db), 4440 + None => Ok(0), 4441 + } 4442 + })(); 4443 + let _ = tx.send(result); 4444 + } 4269 4445 } 4270 4446 } 4271 4447 ··· 4818 4994 .map_err(metastore_to_db); 4819 4995 let _ = tx.send(result); 4820 4996 } 4997 + OAuthRequest::Get2faChallengeCode { request_uri, tx } => { 4998 + let result = state 4999 + .metastore 5000 + .oauth_ops() 5001 + .get_2fa_challenge_code(&request_uri) 5002 + .map_err(metastore_to_db); 5003 + let _ = tx.send(result); 5004 + } 4821 5005 } 4822 5006 } 4823 5007 ··· 5045 5229 } => { 5046 5230 let _ = tx.send( 5047 5231 user.admin_update_password(&did, &password_hash) 5232 + .map_err(metastore_to_db), 5233 + ); 5234 + } 5235 + UserRequest::SetAdminStatus { did, is_admin, tx } => { 5236 + let _ = tx.send( 5237 + user.set_admin_status(&did, is_admin) 5048 5238 .map_err(metastore_to_db), 5049 5239 ); 5050 5240 } ··· 5567 5757 ); 5568 5758 } 5569 5759 UserRequest::DeleteAccountWithFirehose { user_id, did, tx } => { 5570 - let _ = tx.send( 5571 - user.delete_account_with_firehose(user_id, &did) 5572 - .map_err(metastore_to_db), 5573 - ); 5760 + let result = user 5761 + .delete_account_complete(user_id, &did) 5762 + .map_err(metastore_to_db) 5763 + .and_then(|()| { 5764 + state 5765 + .event_ops 5766 + .insert_account_event(&did, AccountStatus::Deleted) 5767 + }); 5768 + let _ = tx.send(result.map(|seq| seq.as_i64())); 5574 5769 } 5575 5770 UserRequest::CreatePasswordAccount { input, tx } => { 5576 5771 let _ = tx.send(user.create_password_account(&input)); 5577 5772 } 5578 5773 UserRequest::CreateDelegatedAccount { input, tx } => { 5579 - let _ = tx.send(user.create_delegated_account(&input)); 5774 + let result = user.create_delegated_account(&input).and_then(|account| { 5775 + let scope = 5776 + tranquil_db_traits::DbScope::new(&input.controller_scopes).map_err(|e| { 5777 + tranquil_db_traits::CreateAccountError::Database(format!( 5778 + "invalid delegation scope: {e}" 5779 + )) 5780 + })?; 5781 + state 5782 + .metastore 5783 + .delegation_ops() 5784 + .create_delegation( 5785 + &input.did, 5786 + &input.controller_did, 5787 + &scope, 5788 + &input.controller_did, 5789 + ) 5790 + .map_err(|e| { 5791 + tranquil_db_traits::CreateAccountError::Database(format!( 5792 + "delegation grant creation failed: {e}" 5793 + )) 5794 + })?; 5795 + Ok(account) 5796 + }); 5797 + let _ = tx.send(result); 5580 5798 } 5581 5799 UserRequest::CreatePasskeyAccount { input, tx } => { 5582 5800 let _ = tx.send(user.create_passkey_account(&input)); ··· 5632 5850 UserRequest::RecoverPasskeyAccount { input, tx } => { 5633 5851 let _ = tx.send( 5634 5852 user.recover_passkey_account(&input) 5853 + .map_err(metastore_to_db), 5854 + ); 5855 + } 5856 + UserRequest::GetPasswordResetInfo { email, tx } => { 5857 + let _ = tx.send( 5858 + user.get_password_reset_info(&email) 5859 + .map_err(metastore_to_db), 5860 + ); 5861 + } 5862 + UserRequest::EnableTotpVerified { 5863 + did, 5864 + encrypted_secret, 5865 + tx, 5866 + } => { 5867 + let _ = tx.send( 5868 + user.enable_totp_verified(&did, &encrypted_secret) 5869 + .map_err(metastore_to_db), 5870 + ); 5871 + } 5872 + UserRequest::SetTwoFactorEnabled { did, enabled, tx } => { 5873 + let _ = tx.send( 5874 + user.set_two_factor_enabled(&did, enabled) 5875 + .map_err(metastore_to_db), 5876 + ); 5877 + } 5878 + UserRequest::ExpirePasswordResetCode { email, tx } => { 5879 + let _ = tx.send( 5880 + user.expire_password_reset_code(&email) 5635 5881 .map_err(metastore_to_db), 5636 5882 ); 5637 5883 }
+209 -5
crates/tranquil-store/src/metastore/infra_ops.rs
··· 22 22 use super::users::UserValue; 23 23 24 24 use tranquil_db_traits::{ 25 - AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest, InviteCodeError, 26 - InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, InviteCodeState, InviteCodeUse, 27 - NotificationHistoryRow, QueuedComms, ReservedSigningKey, ValidatedInviteCode, 25 + AdminAccountInfo, CommsChannel, CommsStatus, CommsType, DeletionRequest, 26 + DeletionRequestWithToken, InviteCodeError, InviteCodeInfo, InviteCodeRow, InviteCodeSortOrder, 27 + InviteCodeState, InviteCodeUse, NotificationHistoryRow, PlcTokenInfo, QueuedComms, 28 + ReservedSigningKey, ReservedSigningKeyFull, ValidatedInviteCode, 28 29 }; 29 30 use tranquil_types::{CidLink, Did, Handle}; 30 31 ··· 713 714 used: false, 714 715 created_at_ms: now_ms, 715 716 expires_at_ms: expires_at.timestamp_millis(), 717 + used_at_ms: None, 716 718 }; 717 719 718 720 let primary_key = signing_key_key(public_key_did_key); ··· 771 773 ))?; 772 774 773 775 val.used = true; 776 + val.used_at_ms = Some(Utc::now().timestamp_millis()); 774 777 self.infra 775 778 .insert(primary_key.as_slice(), val.serialize()) 776 779 .map_err(MetastoreError::Fjall) ··· 906 909 let mut reader = super::encoding::KeyReader::new(&key_bytes); 907 910 reader.tag(); 908 911 reader.bytes(); 909 - let name = reader 912 + let raw_name = reader 910 913 .string() 911 914 .ok_or(MetastoreError::CorruptData("corrupt account pref key"))?; 915 + let name = raw_name 916 + .split('\x00') 917 + .next() 918 + .unwrap_or(&raw_name) 919 + .to_owned(); 912 920 let value: serde_json::Value = serde_json::from_slice(&val_bytes) 913 921 .map_err(|_| MetastoreError::CorruptData("corrupt account pref json"))?; 914 922 acc.push((name, value)); ··· 939 947 Ok::<(), MetastoreError>(()) 940 948 })?; 941 949 950 + let mut counts: std::collections::HashMap<&str, u32> = std::collections::HashMap::new(); 942 951 preferences.iter().try_for_each(|(name, value)| { 943 - let key = account_pref_key(user_id, name); 952 + let idx = counts.entry(name.as_str()).or_insert(0); 953 + let indexed_name = match *idx { 954 + 0 => name.clone(), 955 + n => format!("{}\x00{}", name, n), 956 + }; 957 + *idx += 1; 958 + let key = account_pref_key(user_id, &indexed_name); 944 959 let bytes = serde_json::to_vec(value) 945 960 .map_err(|_| MetastoreError::InvalidInput("invalid json for account preference"))?; 946 961 batch.insert(&self.infra, key.as_slice(), bytes); ··· 1205 1220 } 1206 1221 }) 1207 1222 .collect() 1223 + } 1224 + 1225 + pub fn get_deletion_request_by_did( 1226 + &self, 1227 + did: &Did, 1228 + ) -> Result<Option<DeletionRequestWithToken>, MetastoreError> { 1229 + let did_key = deletion_by_did_key(did.as_str()); 1230 + let token = match self 1231 + .infra 1232 + .get(did_key.as_slice()) 1233 + .map_err(MetastoreError::Fjall)? 1234 + { 1235 + Some(raw) => String::from_utf8(raw.to_vec()) 1236 + .map_err(|_| MetastoreError::CorruptData("deletion by_did not valid utf8"))?, 1237 + None => return Ok(None), 1238 + }; 1239 + 1240 + let primary_key = deletion_request_key(&token); 1241 + let val: Option<DeletionRequestValue> = point_lookup( 1242 + &self.infra, 1243 + primary_key.as_slice(), 1244 + DeletionRequestValue::deserialize, 1245 + "corrupt deletion request", 1246 + )?; 1247 + Ok(val.map(|v| DeletionRequestWithToken { 1248 + token, 1249 + did: Did::new(v.did).expect("valid DID in database"), 1250 + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), 1251 + })) 1252 + } 1253 + 1254 + pub fn get_latest_comms_for_user( 1255 + &self, 1256 + user_id: Uuid, 1257 + comms_type: CommsType, 1258 + limit: i64, 1259 + ) -> Result<Vec<QueuedComms>, MetastoreError> { 1260 + let target_type = comms_type_to_u8(comms_type); 1261 + let limit = usize::try_from(limit).unwrap_or(0); 1262 + let prefix = comms_queue_prefix(); 1263 + 1264 + let mut results: Vec<QueuedComms> = self 1265 + .infra 1266 + .prefix(prefix.as_slice()) 1267 + .map(|guard| -> Result<Option<QueuedComms>, MetastoreError> { 1268 + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; 1269 + let val = QueuedCommsValue::deserialize(&val_bytes) 1270 + .ok_or(MetastoreError::CorruptData("corrupt comms queue entry"))?; 1271 + let matches_user = val.user_id == Some(user_id); 1272 + let matches_type = val.comms_type == target_type; 1273 + match matches_user && matches_type { 1274 + true => Ok(Some(self.value_to_queued_comms(&val)?)), 1275 + false => Ok(None), 1276 + } 1277 + }) 1278 + .filter_map(Result::transpose) 1279 + .collect::<Result<Vec<_>, _>>()?; 1280 + 1281 + results.sort_by_key(|r| std::cmp::Reverse(r.created_at)); 1282 + results.truncate(limit); 1283 + Ok(results) 1284 + } 1285 + 1286 + pub fn count_comms_by_type( 1287 + &self, 1288 + user_id: Uuid, 1289 + comms_type: CommsType, 1290 + ) -> Result<i64, MetastoreError> { 1291 + let target_type = comms_type_to_u8(comms_type); 1292 + let prefix = comms_queue_prefix(); 1293 + 1294 + let count = self 1295 + .infra 1296 + .prefix(prefix.as_slice()) 1297 + .try_fold(0i64, |acc, guard| { 1298 + let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; 1299 + let val = QueuedCommsValue::deserialize(&val_bytes) 1300 + .ok_or(MetastoreError::CorruptData("corrupt comms queue entry"))?; 1301 + let matches = val.user_id == Some(user_id) && val.comms_type == target_type; 1302 + Ok::<i64, MetastoreError>(acc + i64::from(matches)) 1303 + })?; 1304 + 1305 + Ok(count) 1306 + } 1307 + 1308 + pub fn delete_comms_by_type_for_user( 1309 + &self, 1310 + user_id: Uuid, 1311 + comms_type: CommsType, 1312 + ) -> Result<u64, MetastoreError> { 1313 + let target_type = comms_type_to_u8(comms_type); 1314 + let prefix = comms_queue_prefix(); 1315 + 1316 + let keys_to_delete: Vec<Vec<u8>> = self 1317 + .infra 1318 + .prefix(prefix.as_slice()) 1319 + .map(|guard| -> Result<Option<Vec<u8>>, MetastoreError> { 1320 + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; 1321 + let val = QueuedCommsValue::deserialize(&val_bytes) 1322 + .ok_or(MetastoreError::CorruptData("corrupt comms queue entry"))?; 1323 + let matches = val.user_id == Some(user_id) && val.comms_type == target_type; 1324 + match matches { 1325 + true => Ok(Some(key_bytes.to_vec())), 1326 + false => Ok(None), 1327 + } 1328 + }) 1329 + .filter_map(Result::transpose) 1330 + .collect::<Result<Vec<_>, _>>()?; 1331 + 1332 + let count = u64::try_from(keys_to_delete.len()).unwrap_or(u64::MAX); 1333 + let mut batch = self.db.batch(); 1334 + keys_to_delete 1335 + .iter() 1336 + .for_each(|k| batch.remove(&self.infra, k.as_slice())); 1337 + batch.commit().map_err(MetastoreError::Fjall)?; 1338 + Ok(count) 1339 + } 1340 + 1341 + pub fn expire_deletion_request(&self, token: &str) -> Result<(), MetastoreError> { 1342 + let key = deletion_request_key(token); 1343 + let mut val: DeletionRequestValue = point_lookup( 1344 + &self.infra, 1345 + key.as_slice(), 1346 + DeletionRequestValue::deserialize, 1347 + "corrupt deletion request", 1348 + )? 1349 + .ok_or(MetastoreError::InvalidInput("deletion request not found"))?; 1350 + 1351 + val.expires_at_ms = Utc::now().timestamp_millis() - 3_600_000; 1352 + self.infra 1353 + .insert(key.as_slice(), val.serialize()) 1354 + .map_err(MetastoreError::Fjall) 1355 + } 1356 + 1357 + pub fn get_reserved_signing_key_full( 1358 + &self, 1359 + public_key_did_key: &str, 1360 + ) -> Result<Option<ReservedSigningKeyFull>, MetastoreError> { 1361 + let key = signing_key_key(public_key_did_key); 1362 + let val: Option<SigningKeyValue> = point_lookup( 1363 + &self.infra, 1364 + key.as_slice(), 1365 + SigningKeyValue::deserialize, 1366 + "corrupt signing key", 1367 + )?; 1368 + Ok(val.map(|v| ReservedSigningKeyFull { 1369 + id: v.id, 1370 + did: v.did.and_then(|d| Did::new(d).ok()), 1371 + public_key_did_key: v.public_key_did_key, 1372 + private_key_bytes: v.private_key_bytes, 1373 + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), 1374 + used_at: v.used_at_ms.and_then(DateTime::from_timestamp_millis), 1375 + })) 1376 + } 1377 + 1378 + pub fn get_plc_tokens_for_user( 1379 + &self, 1380 + user_id: Uuid, 1381 + ) -> Result<Vec<PlcTokenInfo>, MetastoreError> { 1382 + let prefix = plc_token_prefix(user_id); 1383 + self.infra 1384 + .prefix(prefix.as_slice()) 1385 + .map(|guard| -> Result<PlcTokenInfo, MetastoreError> { 1386 + let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?; 1387 + let arr: [u8; 8] = val_bytes 1388 + .as_ref() 1389 + .try_into() 1390 + .map_err(|_| MetastoreError::CorruptData("plc token expiry not 8 bytes"))?; 1391 + let expires_at = 1392 + DateTime::from_timestamp_millis(i64::from_be_bytes(arr)).unwrap_or_default(); 1393 + let mut reader = super::encoding::KeyReader::new(&key_bytes); 1394 + let _tag = reader.tag(); 1395 + let _user_id = reader.bytes(); 1396 + let token = reader 1397 + .string() 1398 + .ok_or(MetastoreError::CorruptData("plc token key missing token"))?; 1399 + Ok(PlcTokenInfo { token, expires_at }) 1400 + }) 1401 + .collect() 1402 + } 1403 + 1404 + pub fn count_plc_tokens_for_user(&self, user_id: Uuid) -> Result<i64, MetastoreError> { 1405 + let prefix = plc_token_prefix(user_id); 1406 + Ok(self 1407 + .infra 1408 + .prefix(prefix.as_slice()) 1409 + .count() 1410 + .try_into() 1411 + .unwrap_or(0)) 1208 1412 } 1209 1413 }
+52 -3
crates/tranquil-store/src/metastore/infra_schema.rs
··· 7 7 const COMMS_SCHEMA_VERSION: u8 = 1; 8 8 const INVITE_CODE_SCHEMA_VERSION: u8 = 1; 9 9 const INVITE_USE_SCHEMA_VERSION: u8 = 1; 10 - const SIGNING_KEY_SCHEMA_VERSION: u8 = 1; 10 + const SIGNING_KEY_SCHEMA_V1: u8 = 1; 11 + const SIGNING_KEY_SCHEMA_V2: u8 = 2; 11 12 const DELETION_REQUEST_SCHEMA_VERSION: u8 = 1; 12 13 const REPORT_SCHEMA_VERSION: u8 = 1; 13 14 const NOTIFICATION_HISTORY_SCHEMA_VERSION: u8 = 1; ··· 105 106 } 106 107 107 108 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 109 + struct SigningKeyValueV1 { 110 + id: uuid::Uuid, 111 + did: Option<String>, 112 + public_key_did_key: String, 113 + private_key_bytes: Vec<u8>, 114 + used: bool, 115 + created_at_ms: i64, 116 + expires_at_ms: i64, 117 + } 118 + 119 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 108 120 pub struct SigningKeyValue { 109 121 pub id: uuid::Uuid, 110 122 pub did: Option<String>, ··· 113 125 pub used: bool, 114 126 pub created_at_ms: i64, 115 127 pub expires_at_ms: i64, 128 + pub used_at_ms: Option<i64>, 116 129 } 117 130 118 131 impl SigningKeyValue { ··· 120 133 let payload = 121 134 postcard::to_allocvec(self).expect("SigningKeyValue serialization cannot fail"); 122 135 let mut buf = Vec::with_capacity(1 + payload.len()); 123 - buf.push(SIGNING_KEY_SCHEMA_VERSION); 136 + buf.push(SIGNING_KEY_SCHEMA_V2); 124 137 buf.extend_from_slice(&payload); 125 138 buf 126 139 } ··· 128 141 pub fn deserialize(bytes: &[u8]) -> Option<Self> { 129 142 let (&version, payload) = bytes.split_first()?; 130 143 match version { 131 - SIGNING_KEY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(), 144 + SIGNING_KEY_SCHEMA_V1 => { 145 + let v1: SigningKeyValueV1 = postcard::from_bytes(payload).ok()?; 146 + Some(Self { 147 + id: v1.id, 148 + did: v1.did, 149 + public_key_did_key: v1.public_key_did_key, 150 + private_key_bytes: v1.private_key_bytes, 151 + used: v1.used, 152 + created_at_ms: v1.created_at_ms, 153 + expires_at_ms: v1.expires_at_ms, 154 + used_at_ms: None, 155 + }) 156 + } 157 + SIGNING_KEY_SCHEMA_V2 => postcard::from_bytes(payload).ok(), 132 158 _ => None, 133 159 } 134 160 } ··· 518 544 used: false, 519 545 created_at_ms: 1700000000000, 520 546 expires_at_ms: 1700000600000, 547 + used_at_ms: None, 521 548 }; 522 549 let bytes = val.serialize(); 523 550 let decoded = SigningKeyValue::deserialize(&bytes).unwrap(); 524 551 assert_eq!(val, decoded); 552 + } 553 + 554 + #[test] 555 + fn signing_key_value_v1_migration() { 556 + let v1 = SigningKeyValueV1 { 557 + id: uuid::Uuid::new_v4(), 558 + did: Some("did:plc:test".to_owned()), 559 + public_key_did_key: "did:key:z123".to_owned(), 560 + private_key_bytes: vec![1, 2, 3, 4], 561 + used: true, 562 + created_at_ms: 1700000000000, 563 + expires_at_ms: 1700000600000, 564 + }; 565 + let payload = postcard::to_allocvec(&v1).unwrap(); 566 + let mut bytes = Vec::with_capacity(1 + payload.len()); 567 + bytes.push(SIGNING_KEY_SCHEMA_V1); 568 + bytes.extend_from_slice(&payload); 569 + 570 + let decoded = SigningKeyValue::deserialize(&bytes).unwrap(); 571 + assert_eq!(decoded.id, v1.id); 572 + assert!(decoded.used); 573 + assert_eq!(decoded.used_at_ms, None); 525 574 } 526 575 527 576 #[test]
+4
crates/tranquil-store/src/metastore/mod.rs
··· 233 233 &self.partitions[p.index()] 234 234 } 235 235 236 + pub fn signal_keyspace(&self) -> Keyspace { 237 + self.partitions[Partition::Signal.index()].clone() 238 + } 239 + 236 240 pub fn user_hashes(&self) -> &Arc<UserHashMap> { 237 241 &self.user_hashes 238 242 }
+17
crates/tranquil-store/src/metastore/oauth_ops.rs
··· 405 405 family_id: token.family_id, 406 406 }; 407 407 408 + let used_val = UsedRefreshValue { 409 + family_id: token.family_id, 410 + }; 411 + 408 412 let mut batch = self.db.batch(); 409 413 batch.remove( 410 414 &self.auth, ··· 428 432 &self.auth, 429 433 oauth_token_by_prev_refresh_key(&old_refresh).as_slice(), 430 434 index.serialize_with_ttl(token.expires_at_ms), 435 + ); 436 + batch.insert( 437 + &self.auth, 438 + oauth_used_refresh_key(&old_refresh).as_slice(), 439 + used_val.serialize_with_ttl(token.expires_at_ms), 431 440 ); 432 441 batch.insert( 433 442 &self.auth, ··· 1581 1590 Ok(count) 1582 1591 } 1583 1592 } 1593 + } 1594 + 1595 + pub fn get_2fa_challenge_code( 1596 + &self, 1597 + request_uri: &RequestId, 1598 + ) -> Result<Option<String>, MetastoreError> { 1599 + self.get_2fa_challenge(request_uri) 1600 + .map(|opt| opt.map(|c| c.code)) 1584 1601 } 1585 1602 } 1586 1603
+9 -3
crates/tranquil-store/src/metastore/partitions.rs
··· 13 13 Users, 14 14 Infra, 15 15 Indexes, 16 + Signal, 16 17 } 17 18 18 19 impl Partition { 19 - pub const ALL: [Partition; 5] = [ 20 + pub const ALL: [Partition; 6] = [ 20 21 Partition::RepoData, 21 22 Partition::Auth, 22 23 Partition::Users, 23 24 Partition::Infra, 24 25 Partition::Indexes, 26 + Partition::Signal, 25 27 ]; 26 28 27 29 pub const fn index(self) -> usize { ··· 31 33 Self::Users => 2, 32 34 Self::Infra => 3, 33 35 Self::Indexes => 4, 36 + Self::Signal => 5, 34 37 } 35 38 } 36 39 ··· 41 44 Self::Users => "users", 42 45 Self::Infra => "infra", 43 46 Self::Indexes => "indexes", 47 + Self::Signal => "signal", 44 48 } 45 49 } 46 50 ··· 52 56 FilterPolicyEntry::Bloom(BloomConstructionPolicy::BitsPerKey(10.0)), 53 57 ])) 54 58 } 55 - Self::Auth | Self::Users | Self::Infra => KeyspaceCreateOptions::default(), 59 + Self::Auth | Self::Users | Self::Infra | Self::Signal => { 60 + KeyspaceCreateOptions::default() 61 + } 56 62 } 57 63 } 58 64 } ··· 112 118 113 119 #[test] 114 120 fn all_partitions_covered() { 115 - assert_eq!(Partition::ALL.len(), 5); 121 + assert_eq!(Partition::ALL.len(), 6); 116 122 } 117 123 118 124 #[test]
+36 -37
crates/tranquil-store/src/metastore/record_ops.rs
··· 179 179 180 180 let effective_cursor = match query.reverse { 181 181 false => { 182 + if let Some(ck) = cursor_key.as_ref().filter(|ck| ck.as_slice() < range_hi) { 183 + range_hi = ck.as_slice(); 184 + } 185 + None 186 + } 187 + true => { 182 188 let narrowed = cursor_key.as_ref().filter(|ck| ck.as_slice() > range_lo); 183 189 match narrowed { 184 190 Some(ck) => { ··· 188 194 None => None, 189 195 } 190 196 } 191 - true => { 192 - if let Some(ck) = cursor_key.as_ref().filter(|ck| ck.as_slice() < range_hi) { 193 - range_hi = ck.as_slice(); 194 - } 195 - None 196 - } 197 197 }; 198 198 199 199 match range_lo >= range_hi { 200 200 true => Ok(Vec::new()), 201 201 false => match query.reverse { 202 - false => { 202 + false => self.list_records_reverse(range_lo, range_hi, query.limit), 203 + true => { 203 204 self.list_records_forward(range_lo, range_hi, effective_cursor, query.limit) 204 205 } 205 - true => self.list_records_reverse(range_lo, range_hi, query.limit), 206 206 }, 207 207 } 208 208 } ··· 674 674 } 675 675 676 676 #[test] 677 - fn list_records_forward_with_limit() { 677 + fn list_records_default_desc_with_limit() { 678 678 let (_dir, ms) = open_fresh(); 679 679 let (user_id, user_hash) = setup_user(&ms); 680 680 let rec_ops = ms.record_ops(); ··· 699 699 .list_records(&lrq(user_id, &collection, None, 3, false, None, None)) 700 700 .unwrap(); 701 701 assert_eq!(results.len(), 3); 702 - assert_eq!(results[0].rkey.as_str(), "rkey000"); 703 - assert_eq!(results[1].rkey.as_str(), "rkey001"); 702 + assert_eq!(results[0].rkey.as_str(), "rkey004"); 703 + assert_eq!(results[1].rkey.as_str(), "rkey003"); 704 704 assert_eq!(results[2].rkey.as_str(), "rkey002"); 705 705 } 706 706 707 707 #[test] 708 - fn list_records_with_cursor() { 708 + fn list_records_default_desc_with_cursor() { 709 709 let (_dir, ms) = open_fresh(); 710 710 let (user_id, user_hash) = setup_user(&ms); 711 711 let rec_ops = ms.record_ops(); ··· 726 726 .unwrap(); 727 727 batch.commit().unwrap(); 728 728 729 - let cursor = Rkey::from("rkey001".to_string()); 729 + let cursor = Rkey::from("rkey003".to_string()); 730 730 let results = rec_ops 731 731 .list_records(&lrq( 732 732 user_id, ··· 740 740 .unwrap(); 741 741 assert_eq!(results.len(), 3); 742 742 assert_eq!(results[0].rkey.as_str(), "rkey002"); 743 - assert_eq!(results[1].rkey.as_str(), "rkey003"); 744 - assert_eq!(results[2].rkey.as_str(), "rkey004"); 743 + assert_eq!(results[1].rkey.as_str(), "rkey001"); 744 + assert_eq!(results[2].rkey.as_str(), "rkey000"); 745 745 } 746 746 747 747 #[test] 748 - fn list_records_reverse() { 748 + fn list_records_reverse_asc() { 749 749 let (_dir, ms) = open_fresh(); 750 750 let (user_id, user_hash) = setup_user(&ms); 751 751 let rec_ops = ms.record_ops(); ··· 770 770 .list_records(&lrq(user_id, &collection, None, 3, true, None, None)) 771 771 .unwrap(); 772 772 assert_eq!(results.len(), 3); 773 - assert_eq!(results[0].rkey.as_str(), "rkey004"); 774 - assert_eq!(results[1].rkey.as_str(), "rkey003"); 773 + assert_eq!(results[0].rkey.as_str(), "rkey000"); 774 + assert_eq!(results[1].rkey.as_str(), "rkey001"); 775 775 assert_eq!(results[2].rkey.as_str(), "rkey002"); 776 776 } 777 777 778 778 #[test] 779 - fn list_records_reverse_with_cursor() { 779 + fn list_records_reverse_asc_with_cursor() { 780 780 let (_dir, ms) = open_fresh(); 781 781 let (user_id, user_hash) = setup_user(&ms); 782 782 let rec_ops = ms.record_ops(); ··· 797 797 .unwrap(); 798 798 batch.commit().unwrap(); 799 799 800 - let cursor = Rkey::from("rkey003".to_string()); 800 + let cursor = Rkey::from("rkey001".to_string()); 801 801 let results = rec_ops 802 802 .list_records(&lrq( 803 803 user_id, ··· 811 811 .unwrap(); 812 812 assert_eq!(results.len(), 3); 813 813 assert_eq!(results[0].rkey.as_str(), "rkey002"); 814 - assert_eq!(results[1].rkey.as_str(), "rkey001"); 815 - assert_eq!(results[2].rkey.as_str(), "rkey000"); 814 + assert_eq!(results[1].rkey.as_str(), "rkey003"); 815 + assert_eq!(results[2].rkey.as_str(), "rkey004"); 816 816 } 817 817 818 818 #[test] 819 - fn list_records_rkey_range_bounds() { 819 + fn list_records_default_desc_rkey_range_bounds() { 820 820 let (_dir, ms) = open_fresh(); 821 821 let (user_id, user_hash) = setup_user(&ms); 822 822 let rec_ops = ms.record_ops(); ··· 851 851 )) 852 852 .unwrap(); 853 853 assert_eq!(results.len(), 4); 854 - assert_eq!(results[0].rkey.as_str(), "rkey003"); 855 - assert_eq!(results[3].rkey.as_str(), "rkey006"); 854 + assert_eq!(results[0].rkey.as_str(), "rkey006"); 855 + assert_eq!(results[3].rkey.as_str(), "rkey003"); 856 856 } 857 857 858 858 #[test] ··· 1220 1220 .list_records(&lrq(user_id, &collection, None, 100, false, None, None)) 1221 1221 .unwrap(); 1222 1222 let result_rkeys: Vec<&str> = results.iter().map(|r| r.rkey.as_str()).collect(); 1223 - assert_eq!(result_rkeys, ["apple", "banana", "mango", "zebra"]); 1223 + assert_eq!(result_rkeys, ["zebra", "mango", "banana", "apple"]); 1224 1224 } 1225 1225 1226 1226 #[test] ··· 1287 1287 .list_records(&lrq(user_id, &collection, None, 100, false, None, None)) 1288 1288 .unwrap(); 1289 1289 assert_eq!(results.len(), 2); 1290 - assert_eq!(results[0].rkey.as_str(), "abc"); 1291 - assert_eq!(results[1].rkey.as_str(), "abc\x00def"); 1290 + assert_eq!(results[0].rkey.as_str(), "abc\x00def"); 1291 + assert_eq!(results[1].rkey.as_str(), "abc"); 1292 1292 } 1293 1293 1294 1294 #[test] ··· 1341 1341 } 1342 1342 1343 1343 #[test] 1344 - fn list_records_cursor_past_rkey_end_returns_empty() { 1344 + fn list_records_cursor_before_all_returns_empty() { 1345 1345 let (_dir, ms) = open_fresh(); 1346 1346 let (user_id, user_hash) = setup_user(&ms); 1347 1347 let rec_ops = ms.record_ops(); ··· 1362 1362 .unwrap(); 1363 1363 batch.commit().unwrap(); 1364 1364 1365 - let cursor = Rkey::from("rkey010".to_string()); 1366 - let rkey_end = Rkey::from("rkey003".to_string()); 1365 + let cursor = Rkey::from("a".to_string()); 1367 1366 let results = rec_ops 1368 1367 .list_records(&lrq( 1369 1368 user_id, ··· 1372 1371 100, 1373 1372 false, 1374 1373 None, 1375 - Some(&rkey_end), 1374 + None, 1376 1375 )) 1377 1376 .unwrap(); 1378 1377 assert!(results.is_empty()); 1379 1378 } 1380 1379 1381 1380 #[test] 1382 - fn list_records_reverse_with_rkey_bounds() { 1381 + fn list_records_reverse_asc_with_rkey_bounds() { 1383 1382 let (_dir, ms) = open_fresh(); 1384 1383 let (user_id, user_hash) = setup_user(&ms); 1385 1384 let rec_ops = ms.record_ops(); ··· 1414 1413 )) 1415 1414 .unwrap(); 1416 1415 assert_eq!(results.len(), 6); 1417 - assert_eq!(results[0].rkey.as_str(), "rkey007"); 1418 - assert_eq!(results[5].rkey.as_str(), "rkey002"); 1416 + assert_eq!(results[0].rkey.as_str(), "rkey002"); 1417 + assert_eq!(results[5].rkey.as_str(), "rkey007"); 1419 1418 } 1420 1419 1421 1420 #[test] 1422 - fn list_records_reverse_cursor_narrows_range() { 1421 + fn list_records_default_desc_cursor_narrows_range() { 1423 1422 let (_dir, ms) = open_fresh(); 1424 1423 let (user_id, user_hash) = setup_user(&ms); 1425 1424 let rec_ops = ms.record_ops(); ··· 1447 1446 &collection, 1448 1447 Some(&cursor), 1449 1448 3, 1450 - true, 1449 + false, 1451 1450 None, 1452 1451 None, 1453 1452 ))
+156 -29
crates/tranquil-store/src/metastore/user_ops.rs
··· 410 410 return Ok(None); 411 411 } 412 412 413 - let email_match = email_filter.map_or(true, |f| { 414 - val.email.as_deref().map_or(false, |e| e.contains(f)) 415 - }); 416 - let handle_match = handle_filter.map_or(true, |f| val.handle.contains(f)); 413 + let email_match = email_filter 414 + .is_none_or(|f| val.email.as_deref().is_some_and(|e| e.contains(f))); 415 + let handle_match = handle_filter.is_none_or(|f| val.handle.contains(f)); 417 416 418 417 match email_match && handle_match { 419 418 true => Ok(Some(AccountSearchResult { ··· 689 688 pub fn is_account_migrated(&self, did: &Did) -> Result<bool, MetastoreError> { 690 689 Ok(self 691 690 .load_user_by_did(did.as_str())? 692 - .map_or(false, |v| v.migrated_to_pds.is_some())) 691 + .is_some_and(|v| v.migrated_to_pds.is_some())) 693 692 } 694 693 695 694 pub fn has_verified_comms_channel(&self, did: &Did) -> Result<bool, MetastoreError> { 696 695 Ok(self 697 696 .load_user_by_did(did.as_str())? 698 - .map_or(false, |v| v.channel_verification() != 0)) 697 + .is_some_and(|v| v.channel_verification() != 0)) 699 698 } 700 699 701 700 pub fn get_id_by_handle(&self, handle: &Handle) -> Result<Option<Uuid>, MetastoreError> { ··· 861 860 } 862 861 } 863 862 863 + pub fn set_admin_status(&self, did: &Did, is_admin: bool) -> Result<(), MetastoreError> { 864 + let user_hash = self.resolve_hash(did.as_str()); 865 + self.mutate_user(user_hash, |u| { 866 + u.is_admin = is_admin; 867 + })?; 868 + Ok(()) 869 + } 870 + 864 871 pub fn get_notification_prefs( 865 872 &self, 866 873 did: &Did, ··· 1198 1205 telegram_username: &str, 1199 1206 ) -> Result<(), MetastoreError> { 1200 1207 self.mutate_user_by_uuid(user_id, |u| { 1201 - match u.telegram_username.as_deref() == Some(telegram_username) { 1202 - true => u.telegram_verified = true, 1203 - false => {} 1208 + if u.telegram_username.as_deref() == Some(telegram_username) { 1209 + u.telegram_verified = true; 1204 1210 } 1205 1211 })?; 1206 1212 Ok(()) ··· 1212 1218 signal_username: &str, 1213 1219 ) -> Result<(), MetastoreError> { 1214 1220 self.mutate_user_by_uuid(user_id, |u| { 1215 - match u.signal_username.as_deref() == Some(signal_username) { 1216 - true => u.signal_verified = true, 1217 - false => {} 1221 + if u.signal_username.as_deref() == Some(signal_username) { 1222 + u.signal_verified = true; 1218 1223 } 1219 1224 })?; 1220 1225 Ok(()) ··· 1251 1256 pub fn has_totp_enabled(&self, did: &Did) -> Result<bool, MetastoreError> { 1252 1257 Ok(self 1253 1258 .load_user_by_did(did.as_str())? 1254 - .map_or(false, |v| v.totp_enabled)) 1259 + .is_some_and(|v| v.totp_enabled)) 1255 1260 } 1256 1261 1257 1262 pub fn has_passkeys(&self, did: &Did) -> Result<bool, MetastoreError> { ··· 2077 2082 let prefix = [super::keys::KeyTag::USER_RESET_CODE.raw()]; 2078 2083 let keys_to_remove: Vec<Vec<u8>> = self 2079 2084 .auth 2080 - .prefix(&prefix) 2085 + .prefix(prefix) 2081 2086 .filter_map(|guard| { 2082 2087 let (key_bytes, val_bytes) = guard.into_inner().ok()?; 2083 2088 let rc = ResetCodeValue::deserialize(&val_bytes)?; ··· 2566 2571 .collect() 2567 2572 } 2568 2573 2569 - pub fn delete_account_with_firehose( 2570 - &self, 2571 - user_id: Uuid, 2572 - did: &Did, 2573 - ) -> Result<i64, MetastoreError> { 2574 - self.delete_account_complete(user_id, did)?; 2575 - tracing::warn!( 2576 - "delete_account_with_firehose: no firehose event emitted (not yet implemented for tranquil-store)" 2577 - ); 2578 - Ok(0) 2579 - } 2580 - 2574 + #[allow(clippy::too_many_arguments)] 2581 2575 fn build_user_value( 2582 2576 &self, 2583 2577 did: &Did, ··· 2894 2888 .map_err(|e| MigrationReactivationError::Database(e.to_string()))? 2895 2889 .ok_or(MigrationReactivationError::NotFound)?; 2896 2890 2897 - match user.deactivated_at_ms { 2898 - None => return Err(MigrationReactivationError::NotDeactivated), 2899 - Some(_) => {} 2891 + if user.deactivated_at_ms.is_none() { 2892 + return Err(MigrationReactivationError::NotDeactivated); 2900 2893 } 2901 2894 2902 2895 let existing_handle = self ··· 3095 3088 batch.commit().map_err(MetastoreError::Fjall)?; 3096 3089 3097 3090 Ok(RecoverPasskeyAccountResult { passkeys_deleted }) 3091 + } 3092 + 3093 + pub fn get_password_reset_info( 3094 + &self, 3095 + email: &str, 3096 + ) -> Result<Option<tranquil_db_traits::PasswordResetInfo>, MetastoreError> { 3097 + let by_email_key = user_by_email_key(email); 3098 + let user_hash = match self 3099 + .users 3100 + .get(by_email_key.as_slice()) 3101 + .map_err(MetastoreError::Fjall)? 3102 + { 3103 + Some(raw) => { 3104 + let arr: [u8; 8] = raw 3105 + .as_ref() 3106 + .try_into() 3107 + .map_err(|_| MetastoreError::CorruptData("email index not 8 bytes"))?; 3108 + UserHash::from_raw(u64::from_be_bytes(arr)) 3109 + } 3110 + None => return Ok(None), 3111 + }; 3112 + 3113 + let user = match self.load_user(user_hash)? { 3114 + Some(u) => u, 3115 + None => return Ok(None), 3116 + }; 3117 + 3118 + let prefix = [super::keys::KeyTag::USER_RESET_CODE.raw()]; 3119 + let reset_code = self 3120 + .auth 3121 + .prefix(prefix) 3122 + .filter_map(|guard| { 3123 + let (_, val_bytes) = guard.into_inner().ok()?; 3124 + let rc = ResetCodeValue::deserialize(&val_bytes)?; 3125 + match rc.user_id == user.id { 3126 + true => Some(rc), 3127 + false => None, 3128 + } 3129 + }) 3130 + .next(); 3131 + 3132 + Ok(Some(tranquil_db_traits::PasswordResetInfo { 3133 + code: reset_code.as_ref().map(|rc| rc.code.clone()), 3134 + expires_at: reset_code.and_then(|rc| DateTime::from_timestamp_millis(rc.expires_at_ms)), 3135 + })) 3136 + } 3137 + 3138 + pub fn enable_totp_verified( 3139 + &self, 3140 + did: &Did, 3141 + encrypted_secret: &[u8], 3142 + ) -> Result<(), MetastoreError> { 3143 + let user_hash = self.resolve_hash(did.as_str()); 3144 + let key = totp_key(user_hash); 3145 + 3146 + let value = TotpValue { 3147 + secret_encrypted: encrypted_secret.to_vec(), 3148 + encryption_version: 1, 3149 + verified: true, 3150 + last_used_at_ms: None, 3151 + }; 3152 + 3153 + let mut batch = self.db.batch(); 3154 + batch.insert(&self.users, key.as_slice(), value.serialize()); 3155 + 3156 + if let Some(mut user) = self.load_user(user_hash)? { 3157 + user.totp_enabled = true; 3158 + batch.insert( 3159 + &self.users, 3160 + user_primary_key(user_hash).as_slice(), 3161 + user.serialize(), 3162 + ); 3163 + } 3164 + 3165 + batch.commit().map_err(MetastoreError::Fjall) 3166 + } 3167 + 3168 + pub fn set_two_factor_enabled(&self, did: &Did, enabled: bool) -> Result<(), MetastoreError> { 3169 + let user_hash = self.resolve_hash(did.as_str()); 3170 + let mut user = self 3171 + .load_user(user_hash)? 3172 + .ok_or(MetastoreError::InvalidInput("user not found"))?; 3173 + 3174 + user.two_factor_enabled = enabled; 3175 + self.users 3176 + .insert(user_primary_key(user_hash).as_slice(), user.serialize()) 3177 + .map_err(MetastoreError::Fjall) 3178 + } 3179 + 3180 + pub fn expire_password_reset_code(&self, email: &str) -> Result<(), MetastoreError> { 3181 + let by_email_key = user_by_email_key(email); 3182 + let user_hash_bytes = match self 3183 + .users 3184 + .get(by_email_key.as_slice()) 3185 + .map_err(MetastoreError::Fjall)? 3186 + { 3187 + Some(raw) => raw, 3188 + None => return Ok(()), 3189 + }; 3190 + 3191 + let arr: [u8; 8] = user_hash_bytes 3192 + .as_ref() 3193 + .try_into() 3194 + .map_err(|_| MetastoreError::CorruptData("email index not 8 bytes"))?; 3195 + let user_hash = UserHash::from_raw(u64::from_be_bytes(arr)); 3196 + let user = match self.load_user(user_hash)? { 3197 + Some(u) => u, 3198 + None => return Ok(()), 3199 + }; 3200 + 3201 + let prefix = [super::keys::KeyTag::USER_RESET_CODE.raw()]; 3202 + let keys_to_remove: Vec<Vec<u8>> = self 3203 + .auth 3204 + .prefix(prefix) 3205 + .filter_map(|guard| { 3206 + let (key_bytes, val_bytes) = guard.into_inner().ok()?; 3207 + let rc = ResetCodeValue::deserialize(&val_bytes)?; 3208 + match rc.user_id == user.id { 3209 + true => Some(key_bytes.to_vec()), 3210 + false => None, 3211 + } 3212 + }) 3213 + .collect(); 3214 + 3215 + match keys_to_remove.is_empty() { 3216 + true => Ok(()), 3217 + false => { 3218 + let mut batch = self.db.batch(); 3219 + keys_to_remove.iter().for_each(|key| { 3220 + batch.remove(&self.auth, key); 3221 + }); 3222 + batch.commit().map_err(MetastoreError::Fjall) 3223 + } 3224 + } 3098 3225 } 3099 3226 }