Our Personal Data Server from scratch!
0
fork

Configure Feed

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

refactor(tranquil-pds): consolidate repos, rate limits, validation, and auth helpers

authored by did:plc:mb5to35neicxt4gemstoro… and committed by tangled.org 19b0ea19 318818ad

+1678 -406
+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 + }
+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 + }
+14
.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text" 9 + ] 10 + }, 11 + "nullable": [] 12 + }, 13 + "hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5" 14 + }
+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 + }
+28
.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT token, 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": "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": "47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88" 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 + }
+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 + }
+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 + }
+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 + }
+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 + }
+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 + }
+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 + }
+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 + }
+14
.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text" 9 + ] 10 + }, 11 + "nullable": [] 12 + }, 13 + "hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4" 14 + }
+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 + }
+28
.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT password_reset_code, password_reset_code_expires_at 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 + "ordinal": 1, 13 + "name": "password_reset_code_expires_at", 14 + "type_info": "Timestamptz" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + "Text" 20 + ] 21 + }, 22 + "nullable": [ 23 + true, 24 + true 25 + ] 26 + }, 27 + "hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382" 28 + }
-4
crates/tranquil-pds/src/api/error.rs
··· 56 56 TotpAlreadyEnabled, 57 57 TotpNotEnabled, 58 58 InvalidCode(Option<String>), 59 - InvalidChannel, 60 59 IdentifierMismatch, 61 60 NoPasskeys, 62 61 NoChallengeInProgress, ··· 182 181 | Self::InvalidCollection 183 182 | Self::InvalidRecord(_) 184 183 | Self::TotpNotEnabled 185 - | Self::InvalidChannel 186 184 | Self::IdentifierMismatch 187 185 | Self::NoPasskeys 188 186 | Self::NoChallengeInProgress ··· 272 270 Self::TotpAlreadyEnabled => Cow::Borrowed("TotpAlreadyEnabled"), 273 271 Self::TotpNotEnabled => Cow::Borrowed("TotpNotEnabled"), 274 272 Self::InvalidCode(_) => Cow::Borrowed("InvalidCode"), 275 - Self::InvalidChannel => Cow::Borrowed("InvalidChannel"), 276 273 Self::IdentifierMismatch => Cow::Borrowed("IdentifierMismatch"), 277 274 Self::NoPasskeys => Cow::Borrowed("NoPasskeys"), 278 275 Self::NoChallengeInProgress => Cow::Borrowed("NoChallengeInProgress"), ··· 390 387 Self::Forbidden => "Forbidden".into(), 391 388 Self::InvitesDisabled => "Invite codes are disabled on this server".into(), 392 389 Self::InvalidCollection => "Invalid collection".into(), 393 - Self::InvalidChannel => "Invalid notification channel".into(), 394 390 Self::TotpAlreadyEnabled => "TOTP is already enabled".into(), 395 391 Self::TotpNotEnabled => "TOTP is not enabled".into(), 396 392 Self::DuplicateAppPassword => "An app password with this name already exists".into(),
+1 -1
crates/tranquil-pds/src/api/mod.rs
··· 8 8 pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_limit}; 9 9 pub use responses::{ 10 10 AccountsOutput, AuditLogOutput, ControllersOutput, DidResponse, EmailUpdateStatusOutput, 11 - EmptyResponse, EnabledResponse, HasPasswordResponse, InUseOutput, OptionsResponse, 11 + EmptyResponse, HasPasswordResponse, InUseOutput, OptionsResponse, 12 12 PasswordResetOutput, PreferredLocaleOutput, PresetsOutput, StatusResponse, SuccessResponse, 13 13 TokenRequiredResponse, VerifiedResponse, 14 14 };
+3 -3
crates/tranquil-pds/src/api/proxy.rs
··· 229 229 let http_uri = crate::util::build_full_url(&format!("/xrpc{}", uri)); 230 230 231 231 match crate::auth::validate_token_with_dpop( 232 - state.user_repo.as_ref(), 233 - state.oauth_repo.as_ref(), 232 + state.repos.user.as_ref(), 233 + state.repos.oauth.as_ref(), 234 234 &token, 235 235 extracted.scheme, 236 236 dpop_proof, ··· 252 252 253 253 let key_bytes = match auth_user.key_bytes { 254 254 Some(kb) => kb, 255 - None => match state.user_repo.get_user_info_by_did(&auth_user.did).await { 255 + None => match state.repos.user.get_user_info_by_did(&auth_user.did).await { 256 256 Ok(Some(info)) => match info.key_bytes { 257 257 Some(key_bytes_enc) => { 258 258 match crate::config::decrypt_key(
-23
crates/tranquil-pds/src/api/responses.rs
··· 71 71 } 72 72 73 73 #[derive(Debug, Serialize)] 74 - pub struct EnabledResponse { 75 - pub enabled: bool, 76 - } 77 - 78 - impl EnabledResponse { 79 - pub fn response(enabled: bool) -> impl IntoResponse { 80 - Json(Self { enabled }) 81 - } 82 - } 83 - 84 - #[derive(Debug, Serialize)] 85 74 pub struct StatusResponse { 86 75 pub status: String, 87 76 } ··· 91 80 Json(Self { 92 81 status: status.into(), 93 82 }) 94 - } 95 - } 96 - 97 - #[derive(Debug, Serialize)] 98 - #[serde(rename_all = "camelCase")] 99 - pub struct DidDocumentResponse { 100 - pub did_document: serde_json::Value, 101 - } 102 - 103 - impl DidDocumentResponse { 104 - pub fn response(did_document: serde_json::Value) -> impl IntoResponse { 105 - Json(Self { did_document }) 106 83 } 107 84 } 108 85
+1 -150
crates/tranquil-pds/src/api/validation.rs
··· 1 - use serde::{Deserialize, Serialize}; 2 1 use std::fmt; 3 - use std::ops::Deref; 4 2 5 3 pub const MAX_EMAIL_LENGTH: usize = 254; 6 4 pub const MAX_LOCAL_PART_LENGTH: usize = 64; ··· 12 10 pub const MAX_HANDLE_LENGTH: usize = 253; 13 11 pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18; 14 12 15 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 16 - #[serde(try_from = "String", into = "String")] 17 - pub struct ValidatedLocalHandle(String); 18 - 19 - impl ValidatedLocalHandle { 20 - pub fn new(handle: impl AsRef<str>) -> Result<Self, HandleValidationError> { 21 - let validated = validate_short_handle(handle.as_ref())?; 22 - Ok(Self(validated)) 23 - } 24 - 25 - pub fn new_allow_reserved(handle: impl AsRef<str>) -> Result<Self, HandleValidationError> { 26 - let validated = validate_service_handle(handle.as_ref(), ReservedHandlePolicy::Allow)?; 27 - Ok(Self(validated)) 28 - } 29 - 30 - pub fn as_str(&self) -> &str { 31 - &self.0 32 - } 33 - 34 - pub fn into_inner(self) -> String { 35 - self.0 36 - } 37 - } 38 - 39 - impl Deref for ValidatedLocalHandle { 40 - type Target = str; 41 - fn deref(&self) -> &Self::Target { 42 - &self.0 43 - } 44 - } 45 - 46 - impl fmt::Display for ValidatedLocalHandle { 47 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 - write!(f, "{}", self.0) 49 - } 50 - } 51 - 52 - impl TryFrom<String> for ValidatedLocalHandle { 53 - type Error = HandleValidationError; 54 - fn try_from(value: String) -> Result<Self, Self::Error> { 55 - Self::new(value) 56 - } 57 - } 58 - 59 - impl From<ValidatedLocalHandle> for String { 60 - fn from(handle: ValidatedLocalHandle) -> Self { 61 - handle.0 62 - } 63 - } 64 - 65 13 #[derive(Debug, Clone, PartialEq, Eq)] 66 14 pub enum EmailValidationError { 67 15 Empty, ··· 98 46 } 99 47 100 48 impl std::error::Error for EmailValidationError {} 101 - 102 - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 103 - #[serde(try_from = "String", into = "String")] 104 - pub struct ValidatedEmail(String); 105 - 106 - impl ValidatedEmail { 107 - pub fn new(email: impl AsRef<str>) -> Result<Self, EmailValidationError> { 108 - let email = email.as_ref().trim(); 109 - validate_email_detailed(email)?; 110 - Ok(Self(email.to_string())) 111 - } 112 - 113 - pub fn as_str(&self) -> &str { 114 - &self.0 115 - } 116 - 117 - pub fn into_inner(self) -> String { 118 - self.0 119 - } 120 - 121 - pub fn local_part(&self) -> &str { 122 - self.0 123 - .rsplit_once('@') 124 - .map(|(local, _)| local) 125 - .unwrap_or("") 126 - } 127 - 128 - pub fn domain(&self) -> &str { 129 - self.0 130 - .rsplit_once('@') 131 - .map(|(_, domain)| domain) 132 - .unwrap_or("") 133 - } 134 - } 135 - 136 - impl Deref for ValidatedEmail { 137 - type Target = str; 138 - fn deref(&self) -> &Self::Target { 139 - &self.0 140 - } 141 - } 142 - 143 - impl fmt::Display for ValidatedEmail { 144 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 - write!(f, "{}", self.0) 146 - } 147 - } 148 - 149 - impl TryFrom<String> for ValidatedEmail { 150 - type Error = EmailValidationError; 151 - fn try_from(value: String) -> Result<Self, Self::Error> { 152 - Self::new(value) 153 - } 154 - } 155 - 156 - impl From<ValidatedEmail> for String { 157 - fn from(email: ValidatedEmail) -> Self { 158 - email.0 159 - } 160 - } 161 49 162 50 fn validate_email_detailed(email: &str) -> Result<(), EmailValidationError> { 163 51 if email.is_empty() { ··· 386 274 } 387 275 388 276 pub fn is_valid_email(email: &str) -> bool { 389 - let email = email.trim(); 390 - if email.is_empty() || email.len() > MAX_EMAIL_LENGTH { 391 - return false; 392 - } 393 - let parts: Vec<&str> = email.rsplitn(2, '@').collect(); 394 - if parts.len() != 2 { 395 - return false; 396 - } 397 - let domain = parts[0]; 398 - let local = parts[1]; 399 - if local.is_empty() || local.len() > MAX_LOCAL_PART_LENGTH { 400 - return false; 401 - } 402 - if local.starts_with('.') || local.ends_with('.') { 403 - return false; 404 - } 405 - if local.contains("..") { 406 - return false; 407 - } 408 - if !local 409 - .chars() 410 - .all(|c| c.is_ascii_alphanumeric() || EMAIL_LOCAL_SPECIAL_CHARS.contains(c)) 411 - { 412 - return false; 413 - } 414 - if domain.is_empty() || domain.len() > MAX_DOMAIN_LENGTH { 415 - return false; 416 - } 417 - if !domain.contains('.') { 418 - return false; 419 - } 420 - domain.split('.').all(|label| { 421 - !label.is_empty() 422 - && label.len() <= MAX_DOMAIN_LABEL_LENGTH 423 - && !label.starts_with('-') 424 - && !label.ends_with('-') 425 - && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') 426 - }) 277 + validate_email_detailed(email.trim()).is_ok() 427 278 } 428 279 429 280 pub fn is_valid_telegram_username(username: &str) -> bool {
+3 -3
crates/tranquil-pds/src/auth/account_verified.rs
··· 22 22 user: &'a AuthenticatedUser, 23 23 ) -> Result<AccountVerified<'a>, ApiError> { 24 24 let is_verified = state 25 - .user_repo 25 + .repos.user 26 26 .has_verified_comms_channel(&user.did) 27 27 .await 28 28 .unwrap_or(false); ··· 32 32 } 33 33 34 34 let is_delegated = state 35 - .delegation_repo 35 + .repos.delegation 36 36 .is_delegated_account(&user.did) 37 37 .await 38 38 .unwrap_or(false); ··· 45 45 } 46 46 47 47 pub async fn require_not_migrated(state: &AppState, did: &Did) -> Result<(), ApiError> { 48 - match state.user_repo.is_account_migrated(did).await { 48 + match state.repos.user.is_account_migrated(did).await { 49 49 Ok(true) => Err(ApiError::AccountMigrated), 50 50 Ok(false) => Ok(()), 51 51 Err(e) => {
+7 -6
crates/tranquil-pds/src/auth/extractor.rs
··· 232 232 uri: &str, 233 233 ) -> Result<AuthenticatedUser, AuthError> { 234 234 match crate::oauth::verify::verify_oauth_access_token( 235 - state.oauth_repo.as_ref(), 235 + state.repos.oauth.as_ref(), 236 236 token, 237 237 dpop_proof, 238 238 method, ··· 242 242 { 243 243 Ok(result) => { 244 244 let user_info = state 245 - .user_repo 245 + .repos.user 246 246 .get_user_info_by_did(&result.did) 247 247 .await 248 248 .ok() ··· 254 254 ); 255 255 Ok(AuthenticatedUser { 256 256 did: result.did, 257 - key_bytes: user_info.key_bytes.and_then(|kb| { 258 - crate::config::decrypt_key(&kb, user_info.encryption_version).ok() 259 - }), 257 + key_bytes: super::try_decrypt_user_key( 258 + user_info.key_bytes.as_deref(), 259 + user_info.encryption_version, 260 + ), 260 261 is_admin: user_info.is_admin, 261 262 status, 262 263 scope: result.scope, ··· 320 321 .unwrap_or_else(|| parts.uri.path().to_string()); 321 322 let uri = build_full_url(&original_uri); 322 323 323 - match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await { 324 + match validate_bearer_token_for_service_auth(state.repos.user.as_ref(), &extracted.token).await { 324 325 Ok(user) if !user.auth_source.is_oauth() => { 325 326 return Ok(ExtractedAuth::User(user)); 326 327 }
+19 -19
crates/tranquil-pds/src/auth/mfa_verified.rs
··· 76 76 ) -> Result<MfaVerified<'a>, ApiError> { 77 77 use crate::auth::reauth::check_legacy_session_mfa; 78 78 79 - if check_legacy_session_mfa(&*state.session_repo, &user.did).await { 79 + if check_legacy_session_mfa(&*state.repos.session, &user.did).await { 80 80 Ok(MfaVerified::from_session_reauth(user)) 81 81 } else { 82 82 let methods = crate::auth::reauth::get_available_reauth_methods( 83 - &*state.user_repo, 84 - &*state.session_repo, 83 + &*state.repos.user, 84 + &*state.repos.session, 85 85 &user.did, 86 86 ) 87 87 .await; ··· 99 99 use chrono::Utc; 100 100 101 101 let status = state 102 - .session_repo 102 + .repos.session 103 103 .get_session_mfa_status(&user.did) 104 104 .await 105 105 .ok() ··· 114 114 } 115 115 } 116 116 let methods = crate::auth::reauth::get_available_reauth_methods( 117 - &*state.user_repo, 118 - &*state.session_repo, 117 + &*state.repos.user, 118 + &*state.repos.session, 119 119 &user.did, 120 120 ) 121 121 .await; ··· 125 125 } 126 126 None => { 127 127 let methods = crate::auth::reauth::get_available_reauth_methods( 128 - &*state.user_repo, 129 - &*state.session_repo, 128 + &*state.repos.user, 129 + &*state.repos.session, 130 130 &user.did, 131 131 ) 132 132 .await; ··· 144 144 use crate::auth::reauth::check_reauth_required_cached; 145 145 146 146 let has_password = state 147 - .user_repo 147 + .repos.user 148 148 .has_password_by_did(&user.did) 149 149 .await 150 150 .ok() 151 151 .flatten() 152 152 .unwrap_or(false); 153 153 let has_passkeys = state 154 - .user_repo 154 + .repos.user 155 155 .has_passkeys(&user.did) 156 156 .await 157 157 .unwrap_or(false); 158 158 let has_totp = state 159 - .user_repo 159 + .repos.user 160 160 .has_totp_enabled(&user.did) 161 161 .await 162 162 .unwrap_or(false); ··· 167 167 return Ok(None); 168 168 } 169 169 170 - if check_reauth_required_cached(&*state.session_repo, &state.cache, &user.did).await { 170 + if check_reauth_required_cached(&*state.repos.session, &state.cache, &user.did).await { 171 171 let methods = crate::auth::reauth::get_available_reauth_methods( 172 - &*state.user_repo, 173 - &*state.session_repo, 172 + &*state.repos.user, 173 + &*state.repos.session, 174 174 &user.did, 175 175 ) 176 176 .await; ··· 188 188 password: &str, 189 189 ) -> Result<MfaVerified<'a>, crate::api::error::ApiError> { 190 190 let hash = state 191 - .user_repo 191 + .repos.user 192 192 .get_password_hash_by_did(&user.did) 193 193 .await 194 194 .ok() ··· 220 220 221 221 if is_backup_code_format(code) { 222 222 let backup_codes = state 223 - .user_repo 223 + .repos.user 224 224 .get_unused_backup_codes(&user.did) 225 225 .await 226 226 .ok() ··· 233 233 234 234 return match matched { 235 235 Some(row) => { 236 - let _ = state.user_repo.mark_backup_code_used(row.id).await; 236 + let _ = state.repos.user.mark_backup_code_used(row.id).await; 237 237 Ok(MfaVerified::from_recovery_code(user)) 238 238 } 239 239 None => Err(crate::api::error::ApiError::InvalidCode(Some( ··· 242 242 }; 243 243 } 244 244 245 - let verified_record = match state.user_repo.get_totp_record_state(&user.did).await { 245 + let verified_record = match state.repos.user.get_totp_record_state(&user.did).await { 246 246 Ok(Some(TotpRecordState::Verified(record))) => record, 247 247 _ => { 248 248 return Err(crate::api::error::ApiError::TotpNotEnabled); ··· 256 256 .map_err(|_| crate::api::error::ApiError::InternalError(None))?; 257 257 258 258 if verify_totp_code(&secret, code) { 259 - let _ = state.user_repo.update_totp_last_used(&user.did).await; 259 + let _ = state.repos.user.update_totp_last_used(&user.did).await; 260 260 Ok(MfaVerified::from_totp(user)) 261 261 } else { 262 262 Err(crate::api::error::ApiError::InvalidCode(Some(
+18 -14
crates/tranquil-pds/src/auth/mod.rs
··· 61 61 lxm == "*" || lxm == expected 62 62 } 63 63 64 + pub fn try_decrypt_user_key( 65 + key_bytes: Option<&[u8]>, 66 + encryption_version: Option<i32>, 67 + ) -> Option<Vec<u8>> { 68 + match (key_bytes, encryption_version) { 69 + (Some(kb), Some(ev)) => crate::config::decrypt_key(kb, Some(ev)).ok(), 70 + _ => None, 71 + } 72 + } 73 + 64 74 pub fn encrypt_totp_secret(secret: &[u8]) -> Result<Vec<u8>, crate::config::CryptoError> { 65 75 crate::config::encrypt_key(secret) 66 76 } ··· 486 496 487 497 let now = chrono::Utc::now(); 488 498 if oauth_token.expires_at > now { 489 - let key_bytes = if let (Some(kb), Some(ev)) = 490 - (&oauth_token.key_bytes, oauth_token.encryption_version) 491 - { 492 - crate::config::decrypt_key(kb, Some(ev)).ok() 493 - } else { 494 - None 495 - }; 499 + let key_bytes = try_decrypt_user_key( 500 + oauth_token.key_bytes.as_deref(), 501 + oauth_token.encryption_version, 502 + ); 496 503 let did: Did = oauth_token 497 504 .did 498 505 .parse() ··· 592 599 if !allow_takendown && status.is_takendown() { 593 600 return Err(TokenValidationError::AccountTakedown); 594 601 } 595 - let key_bytes = if let (Some(kb), Some(ev)) = 596 - (&user_info.key_bytes, user_info.encryption_version) 597 - { 598 - crate::config::decrypt_key(kb, Some(ev)).ok() 599 - } else { 600 - None 601 - }; 602 + let key_bytes = try_decrypt_user_key( 603 + user_info.key_bytes.as_deref(), 604 + user_info.encryption_version, 605 + ); 602 606 Ok(AuthenticatedUser { 603 607 did: result_did, 604 608 key_bytes,
+1 -1
crates/tranquil-pds/src/delegation/mod.rs
··· 26 26 27 27 pub async fn resolve_identity(state: &AppState, did: &Did) -> Option<ResolvedIdentity> { 28 28 let is_local = state 29 - .user_repo 29 + .repos.user 30 30 .get_by_did(did) 31 31 .await 32 32 .ok()
+2 -2
crates/tranquil-pds/src/delegation/roles.rs
··· 29 29 error_msg: &str, 30 30 ) -> Result<bool, ApiError> { 31 31 let result = if check_is_delegated { 32 - state.delegation_repo.is_delegated_account(did).await 32 + state.repos.delegation.is_delegated_account(did).await 33 33 } else { 34 - state.delegation_repo.controls_any_accounts(did).await 34 + state.repos.delegation.controls_any_accounts(did).await 35 35 }; 36 36 match result { 37 37 Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into())),
+2 -2
crates/tranquil-pds/src/oauth/verify.rs
··· 302 302 .headers 303 303 .get(crate::util::HEADER_DPOP) 304 304 .and_then(|v| v.to_str().ok()); 305 - if let Ok(result) = try_legacy_auth(state.user_repo.as_ref(), token).await { 305 + if let Ok(result) = try_legacy_auth(state.repos.user.as_ref(), token).await { 306 306 return Ok(OAuthUser { 307 307 did: result.did, 308 308 client_id: None, ··· 314 314 let http_method = parts.method.as_str(); 315 315 let http_uri = crate::util::build_full_url(&parts.uri.to_string()); 316 316 match verify_oauth_access_token( 317 - state.oauth_repo.as_ref(), 317 + state.repos.oauth.as_ref(), 318 318 token, 319 319 dpop_proof, 320 320 http_method,
+44 -91
crates/tranquil-pds/src/rate_limit/mod.rs
··· 2 2 3 3 pub use extractor::*; 4 4 5 + use crate::state::RateLimitKind; 5 6 use governor::{ 6 - Quota, RateLimiter, 7 + RateLimiter, 7 8 clock::DefaultClock, 8 9 state::{InMemoryState, NotKeyed, keyed::DefaultKeyedStateStore}, 9 10 }; 10 - use std::{num::NonZeroU32, sync::Arc}; 11 + use std::sync::Arc; 11 12 12 13 pub type KeyedRateLimiter = RateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>; 13 14 pub type GlobalRateLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock>; 15 + 16 + fn keyed_limiter(kind: RateLimitKind) -> Arc<KeyedRateLimiter> { 17 + Arc::new(RateLimiter::keyed(kind.params().to_governor_quota())) 18 + } 14 19 15 20 #[derive(Clone)] 16 21 pub struct RateLimiters { ··· 45 50 impl RateLimiters { 46 51 pub fn new() -> Self { 47 52 Self { 48 - login: Arc::new(RateLimiter::keyed(Quota::per_minute( 49 - const { NonZeroU32::new(10).unwrap() }, 50 - ))), 51 - oauth_token: Arc::new(RateLimiter::keyed(Quota::per_minute( 52 - const { NonZeroU32::new(300).unwrap() }, 53 - ))), 54 - oauth_authorize: Arc::new(RateLimiter::keyed(Quota::per_minute( 55 - const { NonZeroU32::new(10).unwrap() }, 56 - ))), 57 - password_reset: Arc::new(RateLimiter::keyed(Quota::per_hour( 58 - const { NonZeroU32::new(5).unwrap() }, 59 - ))), 60 - account_creation: Arc::new(RateLimiter::keyed(Quota::per_hour( 61 - const { NonZeroU32::new(10).unwrap() }, 62 - ))), 63 - refresh_session: Arc::new(RateLimiter::keyed(Quota::per_minute( 64 - const { NonZeroU32::new(60).unwrap() }, 65 - ))), 66 - reset_password: Arc::new(RateLimiter::keyed(Quota::per_minute( 67 - const { NonZeroU32::new(10).unwrap() }, 68 - ))), 69 - oauth_par: Arc::new(RateLimiter::keyed(Quota::per_minute( 70 - const { NonZeroU32::new(30).unwrap() }, 71 - ))), 72 - oauth_introspect: Arc::new(RateLimiter::keyed(Quota::per_minute( 73 - const { NonZeroU32::new(30).unwrap() }, 74 - ))), 75 - app_password: Arc::new(RateLimiter::keyed(Quota::per_minute( 76 - const { NonZeroU32::new(10).unwrap() }, 77 - ))), 78 - email_update: Arc::new(RateLimiter::keyed(Quota::per_hour( 79 - const { NonZeroU32::new(5).unwrap() }, 80 - ))), 81 - totp_verify: Arc::new(RateLimiter::keyed( 82 - Quota::with_period(std::time::Duration::from_secs(60)) 83 - .unwrap() 84 - .allow_burst(const { NonZeroU32::new(5).unwrap() }), 85 - )), 86 - handle_update: Arc::new(RateLimiter::keyed( 87 - Quota::with_period(std::time::Duration::from_secs(30)) 88 - .unwrap() 89 - .allow_burst(const { NonZeroU32::new(10).unwrap() }), 90 - )), 91 - handle_update_daily: Arc::new(RateLimiter::keyed( 92 - Quota::with_period(std::time::Duration::from_secs(1728)) 93 - .unwrap() 94 - .allow_burst(const { NonZeroU32::new(50).unwrap() }), 95 - )), 96 - verification_check: Arc::new(RateLimiter::keyed(Quota::per_minute( 97 - const { NonZeroU32::new(60).unwrap() }, 98 - ))), 99 - sso_initiate: Arc::new(RateLimiter::keyed(Quota::per_minute( 100 - const { NonZeroU32::new(10).unwrap() }, 101 - ))), 102 - sso_callback: Arc::new(RateLimiter::keyed(Quota::per_minute( 103 - const { NonZeroU32::new(30).unwrap() }, 104 - ))), 105 - sso_unlink: Arc::new(RateLimiter::keyed(Quota::per_minute( 106 - const { NonZeroU32::new(10).unwrap() }, 107 - ))), 108 - oauth_register_complete: Arc::new(RateLimiter::keyed( 109 - Quota::with_period(std::time::Duration::from_secs(60)) 110 - .unwrap() 111 - .allow_burst(const { NonZeroU32::new(5).unwrap() }), 112 - )), 113 - handle_verification: Arc::new(RateLimiter::keyed(Quota::per_minute( 114 - const { NonZeroU32::new(10).unwrap() }, 115 - ))), 53 + login: keyed_limiter(RateLimitKind::Login), 54 + oauth_token: keyed_limiter(RateLimitKind::OAuthToken), 55 + oauth_authorize: keyed_limiter(RateLimitKind::OAuthAuthorize), 56 + password_reset: keyed_limiter(RateLimitKind::PasswordReset), 57 + account_creation: keyed_limiter(RateLimitKind::AccountCreation), 58 + refresh_session: keyed_limiter(RateLimitKind::RefreshSession), 59 + reset_password: keyed_limiter(RateLimitKind::ResetPassword), 60 + oauth_par: keyed_limiter(RateLimitKind::OAuthPar), 61 + oauth_introspect: keyed_limiter(RateLimitKind::OAuthIntrospect), 62 + app_password: keyed_limiter(RateLimitKind::AppPassword), 63 + email_update: keyed_limiter(RateLimitKind::EmailUpdate), 64 + totp_verify: keyed_limiter(RateLimitKind::TotpVerify), 65 + handle_update: keyed_limiter(RateLimitKind::HandleUpdate), 66 + handle_update_daily: keyed_limiter(RateLimitKind::HandleUpdateDaily), 67 + verification_check: keyed_limiter(RateLimitKind::VerificationCheck), 68 + sso_initiate: keyed_limiter(RateLimitKind::SsoInitiate), 69 + sso_callback: keyed_limiter(RateLimitKind::SsoCallback), 70 + sso_unlink: keyed_limiter(RateLimitKind::SsoUnlink), 71 + oauth_register_complete: keyed_limiter(RateLimitKind::OAuthRegisterComplete), 72 + handle_verification: keyed_limiter(RateLimitKind::HandleVerification), 116 73 } 117 74 } 118 75 76 + pub fn override_limit(kind: RateLimitKind, limit: u32) -> Arc<KeyedRateLimiter> { 77 + let mut params = kind.params(); 78 + params.limit = limit; 79 + Arc::new(RateLimiter::keyed(params.to_governor_quota())) 80 + } 81 + 119 82 pub fn with_login_limit(mut self, per_minute: u32) -> Self { 120 - self.login = Arc::new(RateLimiter::keyed(Quota::per_minute( 121 - NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(10).unwrap() }), 122 - ))); 83 + self.login = Self::override_limit(RateLimitKind::Login, per_minute); 123 84 self 124 85 } 125 86 126 87 pub fn with_oauth_token_limit(mut self, per_minute: u32) -> Self { 127 - self.oauth_token = Arc::new(RateLimiter::keyed(Quota::per_minute( 128 - NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(30).unwrap() }), 129 - ))); 88 + self.oauth_token = Self::override_limit(RateLimitKind::OAuthToken, per_minute); 130 89 self 131 90 } 132 91 133 92 pub fn with_oauth_authorize_limit(mut self, per_minute: u32) -> Self { 134 - self.oauth_authorize = Arc::new(RateLimiter::keyed(Quota::per_minute( 135 - NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(10).unwrap() }), 136 - ))); 93 + self.oauth_authorize = Self::override_limit(RateLimitKind::OAuthAuthorize, per_minute); 137 94 self 138 95 } 139 96 140 97 pub fn with_password_reset_limit(mut self, per_hour: u32) -> Self { 141 - self.password_reset = Arc::new(RateLimiter::keyed(Quota::per_hour( 142 - NonZeroU32::new(per_hour).unwrap_or(const { NonZeroU32::new(5).unwrap() }), 143 - ))); 98 + self.password_reset = Self::override_limit(RateLimitKind::PasswordReset, per_hour); 144 99 self 145 100 } 146 101 147 102 pub fn with_account_creation_limit(mut self, per_hour: u32) -> Self { 148 - self.account_creation = Arc::new(RateLimiter::keyed(Quota::per_hour( 149 - NonZeroU32::new(per_hour).unwrap_or(const { NonZeroU32::new(10).unwrap() }), 150 - ))); 103 + self.account_creation = Self::override_limit(RateLimitKind::AccountCreation, per_hour); 151 104 self 152 105 } 153 106 154 107 pub fn with_email_update_limit(mut self, per_hour: u32) -> Self { 155 - self.email_update = Arc::new(RateLimiter::keyed(Quota::per_hour( 156 - NonZeroU32::new(per_hour).unwrap_or(const { NonZeroU32::new(5).unwrap() }), 157 - ))); 108 + self.email_update = Self::override_limit(RateLimitKind::EmailUpdate, per_hour); 158 109 self 159 110 } 160 111 161 112 pub fn with_sso_initiate_limit(mut self, per_minute: u32) -> Self { 162 - self.sso_initiate = Arc::new(RateLimiter::keyed(Quota::per_minute( 163 - NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(10).unwrap() }), 164 - ))); 113 + self.sso_initiate = Self::override_limit(RateLimitKind::SsoInitiate, per_minute); 165 114 self 166 115 } 167 116 } ··· 178 127 179 128 #[test] 180 129 fn test_rate_limiter_exhaustion() { 130 + use governor::Quota; 131 + use std::num::NonZeroU32; 181 132 let limiter = RateLimiter::keyed(Quota::per_minute(const { NonZeroU32::new(2).unwrap() })); 182 133 let key = "test_ip".to_string(); 183 134 ··· 188 139 189 140 #[test] 190 141 fn test_different_keys_have_separate_limits() { 142 + use governor::Quota; 143 + use std::num::NonZeroU32; 191 144 let limiter = RateLimiter::keyed(Quota::per_minute(const { NonZeroU32::new(1).unwrap() })); 192 145 193 146 assert!(limiter.check_key(&"ip1".to_string()).is_ok());
-4
crates/tranquil-pds/src/repo/mod.rs
··· 1 1 pub use tranquil_repo::{PostgresBlockStore, TrackingBlockStore}; 2 - 3 - pub mod tracking { 4 - pub use tranquil_repo::TrackingBlockStore; 5 - }
+16 -35
crates/tranquil-pds/src/repo_ops.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::cid_types::CommitCid; 3 - use crate::repo::tracking::TrackingBlockStore; 3 + use crate::repo::TrackingBlockStore; 4 4 use crate::state::AppState; 5 5 use crate::types::{Did, Handle, Nsid, Rkey}; 6 6 use bytes::Bytes; ··· 85 85 86 86 pub async fn get_current_root_cid(state: &AppState, user_id: Uuid) -> Result<CommitCid, ApiError> { 87 87 let root_cid_str = state 88 - .repo_repo 88 + .repos.repo 89 89 .get_repo_root_cid_by_user_id(user_id) 90 90 .await 91 91 .map_err(|e| { ··· 98 98 } 99 99 100 100 pub fn extract_blob_cids(record: &Value) -> Vec<String> { 101 - let mut blobs = Vec::new(); 102 - extract_blob_cids_recursive(record, &mut blobs); 103 - blobs 104 - } 105 - 106 - fn extract_blob_cids_recursive(value: &Value, blobs: &mut Vec<String>) { 107 - match value { 108 - Value::Object(map) => { 109 - if map.get("$type").and_then(|v| v.as_str()) == Some("blob") 110 - && let Some(ref_obj) = map.get("ref") 111 - && let Some(link) = ref_obj.get("$link").and_then(|v| v.as_str()) 112 - { 113 - blobs.push(link.to_string()); 114 - } 115 - map.values() 116 - .for_each(|v| extract_blob_cids_recursive(v, blobs)); 117 - } 118 - Value::Array(arr) => { 119 - arr.iter() 120 - .for_each(|v| extract_blob_cids_recursive(v, blobs)); 121 - } 122 - _ => {} 123 - } 101 + crate::sync::import::find_blob_refs(record, 0) 102 + .into_iter() 103 + .map(|b| b.cid) 104 + .collect() 124 105 } 125 106 126 107 use crate::types::AtUri; ··· 187 168 let write_lock = state.repo_write_locks.lock(user_id).await; 188 169 189 170 let root_cid_str = state 190 - .repo_repo 171 + .repos.repo 191 172 .get_repo_root_cid_by_user_id(user_id) 192 173 .await 193 174 .map_err(|e| { ··· 273 254 if let Some(controller_did) = params.controller_did 274 255 && let Some(detail) = params.delegation_detail 275 256 && let Err(e) = state 276 - .delegation_repo 257 + .repos.delegation 277 258 .log_delegation_action( 278 259 params.did, 279 260 controller_did, ··· 370 351 obsolete_cids, 371 352 } = params; 372 353 let key_row = state 373 - .user_repo 354 + .repos.user 374 355 .get_user_key_by_id(user_id) 375 356 .await 376 357 .map_err(|e| CommitError::DatabaseError(format!("Failed to fetch signing key: {}", e)))? ··· 504 485 }; 505 486 506 487 let _result = state 507 - .repo_repo 488 + .repos.repo 508 489 .apply_commit(input) 509 490 .await 510 491 .map_err(|e| match e { ··· 526 507 record: &serde_json::Value, 527 508 ) -> Result<(String, Cid), CommitError> { 528 509 let user_id: Uuid = state 529 - .user_repo 510 + .repos.user 530 511 .get_id_by_did(did) 531 512 .await 532 513 .map_err(|e| CommitError::DatabaseError(e.to_string()))? ··· 535 516 let _write_lock = state.repo_write_locks.lock(user_id).await; 536 517 537 518 let root_cid_link = state 538 - .repo_repo 519 + .repos.repo 539 520 .get_repo_root_cid_by_user_id(user_id) 540 521 .await 541 522 .map_err(|e| CommitError::DatabaseError(e.to_string()))? ··· 629 610 handle: Option<&Handle>, 630 611 ) -> Result<SequenceNumber, CommitError> { 631 612 state 632 - .repo_repo 613 + .repos.repo 633 614 .insert_identity_event(did, handle) 634 615 .await 635 616 .map_err(|e| CommitError::DatabaseError(format!("identity event: {}", e))) ··· 640 621 status: tranquil_db_traits::AccountStatus, 641 622 ) -> Result<SequenceNumber, CommitError> { 642 623 state 643 - .repo_repo 624 + .repos.repo 644 625 .insert_account_event(did, status) 645 626 .await 646 627 .map_err(|e| CommitError::DatabaseError(format!("account event: {}", e))) ··· 655 636 .parse() 656 637 .map_err(|_| CommitError::InvalidCid(commit_cid.to_string()))?; 657 638 state 658 - .repo_repo 639 + .repos.repo 659 640 .insert_sync_event(did, &cid_link, rev) 660 641 .await 661 642 .map_err(|e| CommitError::DatabaseError(format!("sync event: {}", e))) ··· 671 652 let commit_cid_link = crate::types::CidLink::from(commit_cid); 672 653 let mst_root_cid_link = crate::types::CidLink::from(mst_root_cid); 673 654 state 674 - .repo_repo 655 + .repos.repo 675 656 .insert_genesis_commit_event(did, &commit_cid_link, &mst_root_cid_link, rev) 676 657 .await 677 658 .map_err(|e| CommitError::DatabaseError(format!("genesis commit event: {}", e)))
+13 -26
crates/tranquil-pds/src/state.rs
··· 16 16 use std::sync::atomic::{AtomicBool, Ordering}; 17 17 use tokio::sync::broadcast; 18 18 use tokio_util::sync::CancellationToken; 19 - use tranquil_db::{ 20 - BacklinkRepository, BlobRepository, DelegationRepository, InfraRepository, OAuthRepository, 21 - PostgresRepositories, RepoEventNotifier, RepoRepository, SessionRepository, SsoRepository, 22 - UserRepository, 23 - }; 19 + use tranquil_db::PostgresRepositories; 24 20 use tranquil_db_traits::SequencedEvent; 25 21 26 22 static RATE_LIMITING_DISABLED: AtomicBool = AtomicBool::new(false); ··· 36 32 #[derive(Clone)] 37 33 pub struct AppState { 38 34 pub repos: Arc<PostgresRepositories>, 39 - pub user_repo: Arc<dyn UserRepository>, 40 - pub oauth_repo: Arc<dyn OAuthRepository>, 41 - pub session_repo: Arc<dyn SessionRepository>, 42 - pub delegation_repo: Arc<dyn DelegationRepository>, 43 - pub repo_repo: Arc<dyn RepoRepository>, 44 - pub blob_repo: Arc<dyn BlobRepository>, 45 - pub infra_repo: Arc<dyn InfraRepository>, 46 - pub backlink_repo: Arc<dyn BacklinkRepository>, 47 - pub event_notifier: Arc<dyn RepoEventNotifier>, 48 35 pub block_store: PostgresBlockStore, 49 36 pub blob_store: Arc<dyn BlobStorage>, 50 37 pub firehose_tx: broadcast::Sender<SequencedEvent>, ··· 54 41 pub cache: Arc<dyn Cache>, 55 42 pub distributed_rate_limiter: Arc<dyn DistributedRateLimiter>, 56 43 pub did_resolver: Arc<DidResolver>, 57 - pub sso_repo: Arc<dyn SsoRepository>, 58 44 pub sso_manager: SsoManager, 59 45 pub webauthn_config: Arc<WebAuthnConfig>, 60 46 pub cross_pds_oauth: Arc<CrossPdsOAuthClient>, ··· 67 53 pub struct RateLimitParams { 68 54 pub limit: u32, 69 55 pub window_ms: u64, 56 + } 57 + 58 + impl RateLimitParams { 59 + pub fn to_governor_quota(self) -> governor::Quota { 60 + use std::num::NonZeroU32; 61 + let burst = NonZeroU32::new(self.limit).unwrap_or(NonZeroU32::MIN); 62 + let period = std::time::Duration::from_millis(self.window_ms); 63 + governor::Quota::with_period(period) 64 + .expect("rate limit window must be non-zero") 65 + .allow_burst(burst) 66 + } 70 67 } 71 68 72 69 #[derive(Debug, Clone, Copy)] ··· 119 116 } 120 117 } 121 118 122 - const fn params(&self) -> RateLimitParams { 119 + pub const fn params(&self) -> RateLimitParams { 123 120 match self { 124 121 Self::Login => RateLimitParams { 125 122 limit: 10, ··· 286 283 ); 287 284 288 285 Self { 289 - user_repo: repos.user.clone(), 290 - oauth_repo: repos.oauth.clone(), 291 - session_repo: repos.session.clone(), 292 - delegation_repo: repos.delegation.clone(), 293 - repo_repo: repos.repo.clone(), 294 - blob_repo: repos.blob.clone(), 295 - infra_repo: repos.infra.clone(), 296 - backlink_repo: repos.backlink.clone(), 297 - event_notifier: repos.event_notifier.clone(), 298 - sso_repo: repos.sso.clone(), 299 286 repos, 300 287 block_store, 301 288 blob_store,
+6 -22
crates/tranquil-pds/src/util.rs
··· 25 25 static TELEGRAM_BOT_USERNAME: OnceLock<String> = OnceLock::new(); 26 26 27 27 pub fn generate_token_code() -> String { 28 - generate_token_code_parts(2, 5) 29 - } 30 - 31 - pub fn generate_token_code_parts(parts: usize, part_len: usize) -> String { 32 28 let mut rng = rand::thread_rng(); 33 29 let chars: Vec<char> = BASE32_ALPHABET.chars().collect(); 34 - 35 - (0..parts) 36 - .map(|_| { 37 - (0..part_len) 38 - .map(|_| chars[rng.gen_range(0..chars.len())]) 39 - .collect::<String>() 40 - }) 41 - .collect::<Vec<_>>() 42 - .join("-") 30 + let gen_segment = |rng: &mut rand::rngs::ThreadRng| -> String { 31 + (0..5) 32 + .map(|_| chars[rng.gen_range(0..chars.len())]) 33 + .collect() 34 + }; 35 + format!("{}-{}", gen_segment(&mut rng), gen_segment(&mut rng)) 43 36 } 44 37 45 38 pub fn parse_repeated_query_param(query: Option<&str>, key: &str) -> Vec<String> { ··· 293 286 .filter(|&c| c != '-') 294 287 .all(|c| BASE32_ALPHABET.contains(c)) 295 288 ); 296 - } 297 - 298 - #[test] 299 - fn test_generate_token_code_parts() { 300 - let code = generate_token_code_parts(3, 4); 301 - let parts: Vec<&str> = code.split('-').collect(); 302 - assert_eq!(parts.len(), 3); 303 - 304 - assert!(parts.iter().all(|part| part.len() == 4)); 305 289 } 306 290 307 291 #[test]