this repo has no description

Functional typesafe backend

lewis 387cc447 0be7770b

Changed files
+5339 -6641
.sqlx
src
tests
+29
.sqlx/query-032ac69a52c0baa269988f662516a54823770aee565f4cf5da2fc1f9b89b6bbb.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT trusted_at, trusted_until FROM oauth_device od\n JOIN oauth_account_device oad ON od.id = oad.device_id\n WHERE od.id = $1 AND oad.did = $2", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "trusted_at", 9 + "type_info": "Timestamptz" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "trusted_until", 14 + "type_info": "Timestamptz" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + "Text", 20 + "Text" 21 + ] 22 + }, 23 + "nullable": [ 24 + true, 25 + true 26 + ] 27 + }, 28 + "hash": "032ac69a52c0baa269988f662516a54823770aee565f4cf5da2fc1f9b89b6bbb" 29 + }
-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-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1", 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": "24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6" 22 - }
-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 - }
-54
.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 - ] 37 - } 38 - } 39 - } 40 - } 41 - ], 42 - "parameters": { 43 - "Left": [ 44 - "Uuid" 45 - ] 46 - }, 47 - "nullable": [ 48 - true, 49 - false, 50 - false 51 - ] 52 - }, 53 - "hash": "4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe" 54 - }
-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-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT body, metadata FROM comms_queue WHERE user_id = $1 AND comms_type = 'channel_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 - "ordinal": 1, 13 - "name": "metadata", 14 - "type_info": "Jsonb" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Uuid" 20 - ] 21 - }, 22 - "nullable": [ 23 - false, 24 - true 25 - ] 26 - }, 27 - "hash": "4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a" 28 - }
-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 - }
-23
.sqlx/query-4d8189361d1da271e300041599561ac07a02ffa9a926f94508d7873c4ca07e65.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT trusted_until FROM oauth_device od\n JOIN oauth_account_device oad ON od.id = oad.device_id\n WHERE od.id = $1 AND oad.did = $2", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "trusted_until", 9 - "type_info": "Timestamptz" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text", 15 - "Text" 16 - ] 17 - }, 18 - "nullable": [ 19 - true 20 - ] 21 - }, 22 - "hash": "4d8189361d1da271e300041599561ac07a02ffa9a926f94508d7873c4ca07e65" 23 - }
-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-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "SELECT body FROM comms_queue WHERE user_id = $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 - "Uuid" 15 - ] 16 - }, 17 - "nullable": [ 18 - false 19 - ] 20 - }, 21 - "hash": "5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45" 22 - }
-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-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 - }
-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 - }
-108
.sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT\n id, user_id, recipient, subject, body,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: CommsType\",\n status as \"status: CommsStatus\"\n FROM comms_queue\n WHERE id = $1\n ", 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": "recipient", 19 - "type_info": "Text" 20 - }, 21 - { 22 - "ordinal": 3, 23 - "name": "subject", 24 - "type_info": "Text" 25 - }, 26 - { 27 - "ordinal": 4, 28 - "name": "body", 29 - "type_info": "Text" 30 - }, 31 - { 32 - "ordinal": 5, 33 - "name": "channel: CommsChannel", 34 - "type_info": { 35 - "Custom": { 36 - "name": "comms_channel", 37 - "kind": { 38 - "Enum": [ 39 - "email", 40 - "discord", 41 - "telegram", 42 - "signal" 43 - ] 44 - } 45 - } 46 - } 47 - }, 48 - { 49 - "ordinal": 6, 50 - "name": "comms_type: CommsType", 51 - "type_info": { 52 - "Custom": { 53 - "name": "comms_type", 54 - "kind": { 55 - "Enum": [ 56 - "welcome", 57 - "email_verification", 58 - "password_reset", 59 - "email_update", 60 - "account_deletion", 61 - "admin_email", 62 - "plc_operation", 63 - "two_factor_code", 64 - "channel_verification", 65 - "passkey_recovery", 66 - "legacy_login_alert", 67 - "migration_verification" 68 - ] 69 - } 70 - } 71 - } 72 - }, 73 - { 74 - "ordinal": 7, 75 - "name": "status: CommsStatus", 76 - "type_info": { 77 - "Custom": { 78 - "name": "comms_status", 79 - "kind": { 80 - "Enum": [ 81 - "pending", 82 - "processing", 83 - "sent", 84 - "failed" 85 - ] 86 - } 87 - } 88 - } 89 - } 90 - ], 91 - "parameters": { 92 - "Left": [ 93 - "Uuid" 94 - ] 95 - }, 96 - "nullable": [ 97 - false, 98 - false, 99 - false, 100 - true, 101 - false, 102 - false, 103 - false, 104 - false 105 - ] 106 - }, 107 - "hash": "9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe" 108 - }
-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 - }
-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 - }
-60
.sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT\n recipient, subject, body,\n comms_type as \"comms_type: CommsType\"\n FROM comms_queue\n WHERE id = $1\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "recipient", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "subject", 14 - "type_info": "Text" 15 - }, 16 - { 17 - "ordinal": 2, 18 - "name": "body", 19 - "type_info": "Text" 20 - }, 21 - { 22 - "ordinal": 3, 23 - "name": "comms_type: CommsType", 24 - "type_info": { 25 - "Custom": { 26 - "name": "comms_type", 27 - "kind": { 28 - "Enum": [ 29 - "welcome", 30 - "email_verification", 31 - "password_reset", 32 - "email_update", 33 - "account_deletion", 34 - "admin_email", 35 - "plc_operation", 36 - "two_factor_code", 37 - "channel_verification", 38 - "passkey_recovery", 39 - "legacy_login_alert", 40 - "migration_verification" 41 - ] 42 - } 43 - } 44 - } 45 - } 46 - ], 47 - "parameters": { 48 - "Left": [ 49 - "Uuid" 50 - ] 51 - }, 52 - "nullable": [ 53 - false, 54 - true, 55 - false, 56 - false 57 - ] 58 - }, 59 - "hash": "b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5" 60 - }
-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 - }
-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 - }
-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 - }
-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 - }
+39 -83
src/api/actor/preferences.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 7 8 }; 8 9 use chrono::{Datelike, NaiveDate, Utc}; 9 10 use serde::{Deserialize, Serialize}; 10 - use serde_json::{Value, json}; 11 + use serde_json::Value; 11 12 12 13 const APP_BSKY_NAMESPACE: &str = "app.bsky"; 13 14 const MAX_PREFERENCES_COUNT: usize = 100; ··· 39 40 ) { 40 41 Some(t) => t, 41 42 None => { 42 - return ( 43 - StatusCode::UNAUTHORIZED, 44 - Json(json!({"error": "AuthenticationRequired"})), 45 - ) 46 - .into_response(); 43 + return ApiError::AuthenticationRequired.into_response(); 47 44 } 48 45 }; 49 46 let auth_user = 50 47 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 51 48 Ok(user) => user, 52 49 Err(_) => { 53 - return ( 54 - StatusCode::UNAUTHORIZED, 55 - Json(json!({"error": "AuthenticationFailed"})), 56 - ) 57 - .into_response(); 50 + return ApiError::AuthenticationFailed(None).into_response(); 58 51 } 59 52 }; 60 53 let has_full_access = auth_user.permissions().has_full_access(); 61 54 let user_id: uuid::Uuid = 62 - match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 55 + match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", &*auth_user.did) 63 56 .fetch_optional(&state.db) 64 57 .await 65 58 { 66 59 Ok(Some(id)) => id, 67 60 _ => { 68 - return ( 69 - StatusCode::INTERNAL_SERVER_ERROR, 70 - Json(json!({"error": "InternalError", "message": "User not found"})), 71 - ) 72 - .into_response(); 61 + return ApiError::InternalError(Some("User not found".into())).into_response(); 73 62 } 74 63 }; 75 64 let prefs_result = sqlx::query!( ··· 81 70 let prefs = match prefs_result { 82 71 Ok(rows) => rows, 83 72 Err(_) => { 84 - return ( 85 - StatusCode::INTERNAL_SERVER_ERROR, 86 - Json(json!({"error": "InternalError", "message": "Failed to fetch preferences"})), 87 - ) 88 - .into_response(); 73 + return ApiError::InternalError(Some("Failed to fetch preferences".into())).into_response(); 89 74 } 90 75 }; 91 76 let mut personal_details_pref: Option<Value> = None; ··· 114 99 .and_then(|v| v.as_str()) 115 100 .and_then(get_age_from_datestring) 116 101 { 117 - let declared_age_pref = json!({ 102 + let declared_age_pref = serde_json::json!({ 118 103 "$type": DECLARED_AGE_PREF, 119 104 "isOverAge13": age >= 13, 120 105 "isOverAge16": age >= 16, ··· 139 124 ) { 140 125 Some(t) => t, 141 126 None => { 142 - return ( 143 - StatusCode::UNAUTHORIZED, 144 - Json(json!({"error": "AuthenticationRequired"})), 145 - ) 146 - .into_response(); 127 + return ApiError::AuthenticationRequired.into_response(); 147 128 } 148 129 }; 149 130 let auth_user = 150 131 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 151 132 Ok(user) => user, 152 133 Err(_) => { 153 - return ( 154 - StatusCode::UNAUTHORIZED, 155 - Json(json!({"error": "AuthenticationFailed"})), 156 - ) 157 - .into_response(); 134 + return ApiError::AuthenticationFailed(None).into_response(); 158 135 } 159 136 }; 160 137 let has_full_access = auth_user.permissions().has_full_access(); 161 138 let user_id: uuid::Uuid = 162 - match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 139 + match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", &*auth_user.did) 163 140 .fetch_optional(&state.db) 164 141 .await 165 142 { 166 143 Ok(Some(id)) => id, 167 144 _ => { 168 - return ( 169 - StatusCode::INTERNAL_SERVER_ERROR, 170 - Json(json!({"error": "InternalError", "message": "User not found"})), 171 - ) 172 - .into_response(); 145 + return ApiError::InternalError(Some("User not found".into())).into_response(); 173 146 } 174 147 }; 175 148 if input.preferences.len() > MAX_PREFERENCES_COUNT { 176 - return ( 177 - StatusCode::BAD_REQUEST, 178 - Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})), 179 - ) 180 - .into_response(); 149 + return ApiError::InvalidRequest(format!( 150 + "Too many preferences: {} exceeds limit of {}", 151 + input.preferences.len(), 152 + MAX_PREFERENCES_COUNT 153 + )) 154 + .into_response(); 181 155 } 182 156 let mut forbidden_prefs: Vec<String> = Vec::new(); 183 157 for pref in &input.preferences { 184 158 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 185 159 if pref_str.len() > MAX_PREFERENCE_SIZE { 186 - return ( 187 - StatusCode::BAD_REQUEST, 188 - Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})), 189 - ) 190 - .into_response(); 160 + return ApiError::InvalidRequest(format!( 161 + "Preference too large: {} bytes exceeds limit of {}", 162 + pref_str.len(), 163 + MAX_PREFERENCE_SIZE 164 + )) 165 + .into_response(); 191 166 } 192 167 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 193 168 Some(t) => t, 194 169 None => { 195 - return ( 196 - StatusCode::BAD_REQUEST, 197 - Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})), 198 - ) 170 + return ApiError::InvalidRequest("Preference is missing a $type".into()) 199 171 .into_response(); 200 172 } 201 173 }; 202 174 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 203 - return ( 204 - StatusCode::BAD_REQUEST, 205 - Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})), 206 - ) 207 - .into_response(); 175 + return ApiError::InvalidRequest(format!( 176 + "Some preferences are not in the {} namespace", 177 + APP_BSKY_NAMESPACE 178 + )) 179 + .into_response(); 208 180 } 209 181 if pref_type == PERSONAL_DETAILS_PREF && !has_full_access { 210 182 forbidden_prefs.push(pref_type.to_string()); 211 183 } 212 184 } 213 185 if !forbidden_prefs.is_empty() { 214 - return ( 215 - StatusCode::BAD_REQUEST, 216 - Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})), 217 - ) 218 - .into_response(); 186 + return ApiError::InvalidRequest(format!( 187 + "Do not have authorization to set preferences: {}", 188 + forbidden_prefs.join(", ") 189 + )) 190 + .into_response(); 219 191 } 220 192 let mut tx = match state.db.begin().await { 221 193 Ok(tx) => tx, 222 194 Err(_) => { 223 - return ( 224 - StatusCode::INTERNAL_SERVER_ERROR, 225 - Json(json!({"error": "InternalError", "message": "Failed to start transaction"})), 226 - ) 227 - .into_response(); 195 + return ApiError::InternalError(Some("Failed to start transaction".into())).into_response(); 228 196 } 229 197 }; 230 198 let delete_result = sqlx::query!( ··· 237 205 .await; 238 206 if delete_result.is_err() { 239 207 let _ = tx.rollback().await; 240 - return ( 241 - StatusCode::INTERNAL_SERVER_ERROR, 242 - Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})), 243 - ) 244 - .into_response(); 208 + return ApiError::InternalError(Some("Failed to clear preferences".into())).into_response(); 245 209 } 246 210 for pref in input.preferences { 247 211 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { ··· 261 225 .await; 262 226 if insert_result.is_err() { 263 227 let _ = tx.rollback().await; 264 - return ( 265 - StatusCode::INTERNAL_SERVER_ERROR, 266 - Json(json!({"error": "InternalError", "message": "Failed to save preference"})), 267 - ) 268 - .into_response(); 228 + return ApiError::InternalError(Some("Failed to save preference".into())).into_response(); 269 229 } 270 230 } 271 231 if tx.commit().await.is_err() { 272 - return ( 273 - StatusCode::INTERNAL_SERVER_ERROR, 274 - Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})), 275 - ) 276 - .into_response(); 232 + return ApiError::InternalError(Some("Failed to commit transaction".into())).into_response(); 277 233 } 278 234 StatusCode::OK.into_response() 279 235 }
+21 -71
src/api/admin/account/delete.rs
··· 1 + use crate::api::error::ApiError; 2 + use crate::api::EmptyResponse; 1 3 use crate::auth::BearerAuthAdmin; 2 4 use crate::state::AppState; 5 + use crate::types::Did; 3 6 use axum::{ 4 7 Json, 5 8 extract::State, 6 - http::StatusCode, 7 9 response::{IntoResponse, Response}, 8 10 }; 9 11 use serde::Deserialize; 10 - use serde_json::json; 11 12 use tracing::{error, warn}; 12 13 13 14 #[derive(Deserialize)] 14 15 pub struct DeleteAccountInput { 15 - pub did: String, 16 + pub did: Did, 16 17 } 17 18 18 19 pub async fn delete_account( ··· 20 21 _auth: BearerAuthAdmin, 21 22 Json(input): Json<DeleteAccountInput>, 22 23 ) -> Response { 23 - let did = input.did.trim(); 24 - if did.is_empty() { 25 - return ( 26 - StatusCode::BAD_REQUEST, 27 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 28 - ) 29 - .into_response(); 30 - } 31 - let user = sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did) 24 + let did = &input.did; 25 + let user = sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did.as_str()) 32 26 .fetch_optional(&state.db) 33 27 .await; 34 28 let (user_id, handle) = match user { 35 29 Ok(Some(row)) => (row.id, row.handle), 36 30 Ok(None) => { 37 - return ( 38 - StatusCode::NOT_FOUND, 39 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 40 - ) 41 - .into_response(); 31 + return ApiError::AccountNotFound.into_response(); 42 32 } 43 33 Err(e) => { 44 34 error!("DB error in delete_account: {:?}", e); 45 - return ( 46 - StatusCode::INTERNAL_SERVER_ERROR, 47 - Json(json!({"error": "InternalError"})), 48 - ) 49 - .into_response(); 35 + return ApiError::InternalError(None).into_response(); 50 36 } 51 37 }; 52 38 let mut tx = match state.db.begin().await { 53 39 Ok(tx) => tx, 54 40 Err(e) => { 55 41 error!("Failed to begin transaction for account deletion: {:?}", e); 56 - return ( 57 - StatusCode::INTERNAL_SERVER_ERROR, 58 - Json(json!({"error": "InternalError"})), 59 - ) 60 - .into_response(); 42 + return ApiError::InternalError(None).into_response(); 61 43 } 62 44 }; 63 - if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did) 45 + if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did.as_str()) 64 46 .execute(&mut *tx) 65 47 .await 66 48 { 67 49 error!("Failed to delete session tokens for {}: {:?}", did, e); 68 - return ( 69 - StatusCode::INTERNAL_SERVER_ERROR, 70 - Json(json!({"error": "InternalError", "message": "Failed to delete session tokens"})), 71 - ) 72 - .into_response(); 50 + return ApiError::InternalError(Some("Failed to delete session tokens".into())).into_response(); 73 51 } 74 - if let Err(e) = sqlx::query!("DELETE FROM used_refresh_tokens WHERE session_id IN (SELECT id FROM session_tokens WHERE did = $1)", did) 52 + if let Err(e) = sqlx::query!("DELETE FROM used_refresh_tokens WHERE session_id IN (SELECT id FROM session_tokens WHERE did = $1)", did.as_str()) 75 53 .execute(&mut *tx) 76 54 .await 77 55 { ··· 82 60 .await 83 61 { 84 62 error!("Failed to delete records for user {}: {:?}", user_id, e); 85 - return ( 86 - StatusCode::INTERNAL_SERVER_ERROR, 87 - Json(json!({"error": "InternalError", "message": "Failed to delete records"})), 88 - ) 89 - .into_response(); 63 + return ApiError::InternalError(Some("Failed to delete records".into())).into_response(); 90 64 } 91 65 if let Err(e) = sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id) 92 66 .execute(&mut *tx) 93 67 .await 94 68 { 95 69 error!("Failed to delete repos for user {}: {:?}", user_id, e); 96 - return ( 97 - StatusCode::INTERNAL_SERVER_ERROR, 98 - Json(json!({"error": "InternalError", "message": "Failed to delete repos"})), 99 - ) 100 - .into_response(); 70 + return ApiError::InternalError(Some("Failed to delete repos".into())).into_response(); 101 71 } 102 72 if let Err(e) = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) 103 73 .execute(&mut *tx) 104 74 .await 105 75 { 106 76 error!("Failed to delete blobs for user {}: {:?}", user_id, e); 107 - return ( 108 - StatusCode::INTERNAL_SERVER_ERROR, 109 - Json(json!({"error": "InternalError", "message": "Failed to delete blobs"})), 110 - ) 111 - .into_response(); 77 + return ApiError::InternalError(Some("Failed to delete blobs".into())).into_response(); 112 78 } 113 79 if let Err(e) = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id) 114 80 .execute(&mut *tx) ··· 118 84 "Failed to delete app passwords for user {}: {:?}", 119 85 user_id, e 120 86 ); 121 - return ( 122 - StatusCode::INTERNAL_SERVER_ERROR, 123 - Json(json!({"error": "InternalError", "message": "Failed to delete app passwords"})), 124 - ) 125 - .into_response(); 87 + return ApiError::InternalError(Some("Failed to delete app passwords".into())).into_response(); 126 88 } 127 89 if let Err(e) = sqlx::query!( 128 90 "DELETE FROM invite_code_uses WHERE used_by_user = $1", ··· 153 115 .await 154 116 { 155 117 error!("Failed to delete user keys for user {}: {:?}", user_id, e); 156 - return ( 157 - StatusCode::INTERNAL_SERVER_ERROR, 158 - Json(json!({"error": "InternalError", "message": "Failed to delete user keys"})), 159 - ) 160 - .into_response(); 118 + return ApiError::InternalError(Some("Failed to delete user keys".into())).into_response(); 161 119 } 162 120 if let Err(e) = sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 163 121 .execute(&mut *tx) 164 122 .await 165 123 { 166 124 error!("Failed to delete user {}: {:?}", user_id, e); 167 - return ( 168 - StatusCode::INTERNAL_SERVER_ERROR, 169 - Json(json!({"error": "InternalError", "message": "Failed to delete user"})), 170 - ) 171 - .into_response(); 125 + return ApiError::InternalError(Some("Failed to delete user".into())).into_response(); 172 126 } 173 127 if let Err(e) = tx.commit().await { 174 128 error!("Failed to commit account deletion transaction: {:?}", e); 175 - return ( 176 - StatusCode::INTERNAL_SERVER_ERROR, 177 - Json(json!({"error": "InternalError", "message": "Failed to commit deletion"})), 178 - ) 179 - .into_response(); 129 + return ApiError::InternalError(Some("Failed to commit deletion".into())).into_response(); 180 130 } 181 131 if let Err(e) = 182 - crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await 132 + crate::api::repo::record::sequence_account_event(&state, did.as_str(), false, Some("deleted")).await 183 133 { 184 134 warn!( 185 135 "Failed to sequence account deletion event for {}: {}", ··· 187 137 ); 188 138 } 189 139 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 190 - (StatusCode::OK, Json(json!({}))).into_response() 140 + EmptyResponse::ok().into_response() 191 141 }
+11 -34
src/api/admin/account/email.rs
··· 1 + use crate::api::error::{ApiError, AtpJson}; 1 2 use crate::auth::BearerAuthAdmin; 2 3 use crate::state::AppState; 4 + use crate::types::Did; 3 5 use axum::{ 4 6 Json, 5 7 extract::State, ··· 7 9 response::{IntoResponse, Response}, 8 10 }; 9 11 use serde::{Deserialize, Serialize}; 10 - use serde_json::json; 11 12 use tracing::{error, warn}; 12 13 13 14 #[derive(Deserialize)] 14 15 #[serde(rename_all = "camelCase")] 15 16 pub struct SendEmailInput { 16 - pub recipient_did: String, 17 - pub sender_did: String, 17 + pub recipient_did: Did, 18 + pub sender_did: Did, 18 19 pub content: String, 19 20 pub subject: Option<String>, 20 21 pub comment: Option<String>, ··· 28 29 pub async fn send_email( 29 30 State(state): State<AppState>, 30 31 _auth: BearerAuthAdmin, 31 - Json(input): Json<SendEmailInput>, 32 + AtpJson(input): AtpJson<SendEmailInput>, 32 33 ) -> Response { 33 - let recipient_did = input.recipient_did.trim(); 34 34 let content = input.content.trim(); 35 - if recipient_did.is_empty() { 36 - return ( 37 - StatusCode::BAD_REQUEST, 38 - Json(json!({"error": "InvalidRequest", "message": "recipientDid is required"})), 39 - ) 40 - .into_response(); 41 - } 42 35 if content.is_empty() { 43 - return ( 44 - StatusCode::BAD_REQUEST, 45 - Json(json!({"error": "InvalidRequest", "message": "content is required"})), 46 - ) 47 - .into_response(); 36 + return ApiError::InvalidRequest("content is required".into()).into_response(); 48 37 } 49 38 let user = sqlx::query!( 50 39 "SELECT id, email, handle FROM users WHERE did = $1", 51 - recipient_did 40 + input.recipient_did.as_str() 52 41 ) 53 42 .fetch_optional(&state.db) 54 43 .await; ··· 57 46 let email = match row.email { 58 47 Some(e) => e, 59 48 None => { 60 - return ( 61 - StatusCode::BAD_REQUEST, 62 - Json(json!({"error": "NoEmail", "message": "Recipient has no email address"})), 63 - ) 64 - .into_response(); 49 + return ApiError::NoEmail.into_response(); 65 50 } 66 51 }; 67 52 (row.id, email, row.handle) 68 53 } 69 54 Ok(None) => { 70 - return ( 71 - StatusCode::NOT_FOUND, 72 - Json(json!({"error": "AccountNotFound", "message": "Recipient account not found"})), 73 - ) 74 - .into_response(); 55 + return ApiError::AccountNotFound.into_response(); 75 56 } 76 57 Err(e) => { 77 58 error!("DB error in send_email: {:?}", e); 78 - return ( 79 - StatusCode::INTERNAL_SERVER_ERROR, 80 - Json(json!({"error": "InternalError"})), 81 - ) 82 - .into_response(); 59 + return ApiError::InternalError(None).into_response(); 83 60 } 84 61 }; 85 62 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 97 74 let result = crate::comms::enqueue_comms(&state.db, item).await; 98 75 match result { 99 76 Ok(_) => { 100 - tracing::info!("Admin email queued for {} ({})", handle, recipient_did); 77 + tracing::info!("Admin email queued for {} ({})", handle, input.recipient_did); 101 78 (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 102 79 } 103 80 Err(e) => {
+23 -46
src/api/admin/account/info.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::auth::BearerAuthAdmin; 2 3 use crate::state::AppState; 4 + use crate::types::{Did, Handle}; 3 5 use axum::{ 4 6 Json, 5 7 extract::{Query, RawQuery, State}, ··· 7 9 response::{IntoResponse, Response}, 8 10 }; 9 11 use serde::{Deserialize, Serialize}; 10 - use serde_json::json; 11 12 use tracing::error; 12 13 13 14 #[derive(Deserialize)] 14 15 pub struct GetAccountInfoParams { 15 - pub did: String, 16 + pub did: Did, 16 17 } 17 18 18 19 #[derive(Serialize)] 19 20 #[serde(rename_all = "camelCase")] 20 21 pub struct AccountInfo { 21 - pub did: String, 22 - pub handle: String, 22 + pub did: Did, 23 + pub handle: Handle, 23 24 #[serde(skip_serializing_if = "Option::is_none")] 24 25 pub email: Option<String>, 25 26 pub indexed_at: String, ··· 42 43 pub code: String, 43 44 pub available: i32, 44 45 pub disabled: bool, 45 - pub for_account: String, 46 - pub created_by: String, 46 + pub for_account: Did, 47 + pub created_by: Did, 47 48 pub created_at: String, 48 49 pub uses: Vec<InviteCodeUseInfo>, 49 50 } ··· 51 52 #[derive(Serialize, Clone)] 52 53 #[serde(rename_all = "camelCase")] 53 54 pub struct InviteCodeUseInfo { 54 - pub used_by: String, 55 + pub used_by: Did, 55 56 pub used_at: String, 56 57 } 57 58 ··· 66 67 _auth: BearerAuthAdmin, 67 68 Query(params): Query<GetAccountInfoParams>, 68 69 ) -> Response { 69 - let did = params.did.trim(); 70 - if did.is_empty() { 71 - return ( 72 - StatusCode::BAD_REQUEST, 73 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 74 - ) 75 - .into_response(); 76 - } 77 70 let result = sqlx::query!( 78 71 r#" 79 72 SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at 80 73 FROM users 81 74 WHERE did = $1 82 75 "#, 83 - did 76 + params.did.as_str() 84 77 ) 85 78 .fetch_optional(&state.db) 86 79 .await; ··· 91 84 ( 92 85 StatusCode::OK, 93 86 Json(AccountInfo { 94 - did: row.did, 95 - handle: row.handle, 87 + did: row.did.into(), 88 + handle: row.handle.into(), 96 89 email: row.email, 97 90 indexed_at: row.created_at.to_rfc3339(), 98 91 invite_note: None, ··· 109 102 ) 110 103 .into_response() 111 104 } 112 - Ok(None) => ( 113 - StatusCode::NOT_FOUND, 114 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 115 - ) 116 - .into_response(), 105 + Ok(None) => ApiError::AccountNotFound.into_response(), 117 106 Err(e) => { 118 107 error!("DB error in get_account_info: {:?}", e); 119 - ( 120 - StatusCode::INTERNAL_SERVER_ERROR, 121 - Json(json!({"error": "InternalError"})), 122 - ) 123 - .into_response() 108 + ApiError::InternalError(None).into_response() 124 109 } 125 110 } 126 111 } ··· 199 184 code: row.code, 200 185 available: row.available_uses, 201 186 disabled: row.disabled.unwrap_or(false), 202 - for_account: row.for_account, 203 - created_by: row.created_by, 187 + for_account: row.for_account.into(), 188 + created_by: row.created_by.into(), 204 189 created_at: row.created_at.to_rfc3339(), 205 190 uses: uses 206 191 .into_iter() 207 192 .map(|u| InviteCodeUseInfo { 208 - used_by: u.used_by, 193 + used_by: u.used_by.into(), 209 194 used_at: u.used_at.to_rfc3339(), 210 195 }) 211 196 .collect(), ··· 222 207 .filter(|d| !d.is_empty()) 223 208 .collect(); 224 209 if dids.is_empty() { 225 - return ( 226 - StatusCode::BAD_REQUEST, 227 - Json(json!({"error": "InvalidRequest", "message": "dids is required"})), 228 - ) 229 - .into_response(); 210 + return ApiError::InvalidRequest("dids is required".into()).into_response(); 230 211 } 231 212 let users = match sqlx::query!( 232 213 r#" ··· 242 223 Ok(rows) => rows, 243 224 Err(e) => { 244 225 error!("Failed to fetch account infos: {:?}", e); 245 - return ( 246 - StatusCode::INTERNAL_SERVER_ERROR, 247 - Json(json!({"error": "InternalError"})), 248 - ) 249 - .into_response(); 226 + return ApiError::InternalError(None).into_response(); 250 227 } 251 228 }; 252 229 ··· 306 283 .entry(u.code.clone()) 307 284 .or_default() 308 285 .push(InviteCodeUseInfo { 309 - used_by: u.used_by, 286 + used_by: u.used_by.into(), 310 287 used_at: u.used_at.to_rfc3339(), 311 288 }); 312 289 } ··· 320 297 code: ic.code.clone(), 321 298 available: ic.available_uses, 322 299 disabled: ic.disabled.unwrap_or(false), 323 - for_account: ic.for_account, 324 - created_by: ic.created_by, 300 + for_account: ic.for_account.into(), 301 + created_by: ic.created_by.into(), 325 302 created_at: ic.created_at.to_rfc3339(), 326 303 uses: uses_by_code.get(&ic.code).cloned().unwrap_or_default(), 327 304 }; ··· 339 316 .and_then(|code| code_info_map.get(code).cloned()); 340 317 let invites = codes_by_user.get(&row.id).cloned(); 341 318 infos.push(AccountInfo { 342 - did: row.did, 343 - handle: row.handle, 319 + did: row.did.into(), 320 + handle: row.handle.into(), 344 321 email: row.email, 345 322 indexed_at: row.created_at.to_rfc3339(), 346 323 invite_note: None,
+8 -11
src/api/admin/account/search.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::auth::BearerAuthAdmin; 2 3 use crate::state::AppState; 4 + use crate::types::{Did, Handle}; 3 5 use axum::{ 4 6 Json, 5 7 extract::{Query, State}, ··· 7 9 response::{IntoResponse, Response}, 8 10 }; 9 11 use serde::{Deserialize, Serialize}; 10 - use serde_json::json; 11 12 use tracing::error; 12 13 13 14 #[derive(Deserialize)] ··· 26 27 #[derive(Serialize)] 27 28 #[serde(rename_all = "camelCase")] 28 29 pub struct AccountView { 29 - pub did: String, 30 - pub handle: String, 30 + pub did: Did, 31 + pub handle: Handle, 31 32 #[serde(skip_serializing_if = "Option::is_none")] 32 33 pub email: Option<String>, 33 34 pub indexed_at: String, ··· 101 102 invites_disabled, 102 103 )| { 103 104 AccountView { 104 - did: did.clone(), 105 - handle, 105 + did: did.clone().into(), 106 + handle: handle.into(), 106 107 email, 107 108 indexed_at: created_at.to_rfc3339(), 108 109 email_confirmed_at: if email_verified { ··· 117 118 ) 118 119 .collect(); 119 120 let next_cursor = if has_more { 120 - accounts.last().map(|a| a.did.clone()) 121 + accounts.last().map(|a| a.did.to_string()) 121 122 } else { 122 123 None 123 124 }; ··· 132 133 } 133 134 Err(e) => { 134 135 error!("DB error in search_accounts: {:?}", e); 135 - ( 136 - StatusCode::INTERNAL_SERVER_ERROR, 137 - Json(json!({"error": "InternalError"})), 138 - ) 139 - .into_response() 136 + ApiError::InternalError(None).into_response() 140 137 } 141 138 } 142 139 }
+31 -80
src/api/admin/account/update.rs
··· 1 + use crate::api::error::ApiError; 2 + use crate::api::EmptyResponse; 1 3 use crate::auth::BearerAuthAdmin; 2 4 use crate::state::AppState; 5 + use crate::types::{Did, PlainPassword}; 3 6 use axum::{ 4 7 Json, 5 8 extract::State, 6 - http::StatusCode, 7 9 response::{IntoResponse, Response}, 8 10 }; 9 11 use serde::Deserialize; 10 - use serde_json::json; 11 12 use tracing::{error, warn}; 12 13 13 14 #[derive(Deserialize)] ··· 24 25 let account = input.account.trim(); 25 26 let email = input.email.trim(); 26 27 if account.is_empty() || email.is_empty() { 27 - return ( 28 - StatusCode::BAD_REQUEST, 29 - Json(json!({"error": "InvalidRequest", "message": "account and email are required"})), 30 - ) 31 - .into_response(); 28 + return ApiError::InvalidRequest("account and email are required".into()).into_response(); 32 29 } 33 30 let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account) 34 31 .execute(&state.db) ··· 36 33 match result { 37 34 Ok(r) => { 38 35 if r.rows_affected() == 0 { 39 - return ( 40 - StatusCode::NOT_FOUND, 41 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 42 - ) 43 - .into_response(); 36 + return ApiError::AccountNotFound.into_response(); 44 37 } 45 - (StatusCode::OK, Json(json!({}))).into_response() 38 + EmptyResponse::ok().into_response() 46 39 } 47 40 Err(e) => { 48 41 error!("DB error updating email: {:?}", e); 49 - ( 50 - StatusCode::INTERNAL_SERVER_ERROR, 51 - Json(json!({"error": "InternalError"})), 52 - ) 53 - .into_response() 42 + ApiError::InternalError(None).into_response() 54 43 } 55 44 } 56 45 } 57 46 58 47 #[derive(Deserialize)] 59 48 pub struct UpdateAccountHandleInput { 60 - pub did: String, 49 + pub did: Did, 61 50 pub handle: String, 62 51 } 63 52 ··· 66 55 _auth: BearerAuthAdmin, 67 56 Json(input): Json<UpdateAccountHandleInput>, 68 57 ) -> Response { 69 - let did = input.did.trim(); 58 + let did = &input.did; 70 59 let input_handle = input.handle.trim(); 71 - if did.is_empty() || input_handle.is_empty() { 72 - return ( 73 - StatusCode::BAD_REQUEST, 74 - Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})), 75 - ) 76 - .into_response(); 60 + if input_handle.is_empty() { 61 + return ApiError::InvalidRequest("handle is required".into()).into_response(); 77 62 } 78 63 if !input_handle 79 64 .chars() 80 65 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 81 66 { 82 - return ( 83 - StatusCode::BAD_REQUEST, 84 - Json( 85 - json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}), 86 - ), 87 - ) 88 - .into_response(); 67 + return ApiError::InvalidHandle(None).into_response(); 89 68 } 90 69 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 91 70 let handle = if !input_handle.contains('.') { ··· 93 72 } else { 94 73 input_handle.to_string() 95 74 }; 96 - let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did) 75 + let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did.as_str()) 97 76 .fetch_optional(&state.db) 98 77 .await 99 78 .ok() ··· 101 80 let existing = sqlx::query!( 102 81 "SELECT id FROM users WHERE handle = $1 AND did != $2", 103 82 handle, 104 - did 83 + did.as_str() 105 84 ) 106 85 .fetch_optional(&state.db) 107 86 .await; 108 87 if let Ok(Some(_)) = existing { 109 - return ( 110 - StatusCode::BAD_REQUEST, 111 - Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})), 112 - ) 113 - .into_response(); 88 + return ApiError::HandleTaken.into_response(); 114 89 } 115 - let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did) 90 + let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did.as_str()) 116 91 .execute(&state.db) 117 92 .await; 118 93 match result { 119 94 Ok(r) => { 120 95 if r.rows_affected() == 0 { 121 - return ( 122 - StatusCode::NOT_FOUND, 123 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 124 - ) 125 - .into_response(); 96 + return ApiError::AccountNotFound.into_response(); 126 97 } 127 98 if let Some(old) = old_handle { 128 99 let _ = state.cache.delete(&format!("handle:{}", old)).await; 129 100 } 130 101 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 131 102 if let Err(e) = 132 - crate::api::repo::record::sequence_identity_event(&state, did, Some(&handle)).await 103 + crate::api::repo::record::sequence_identity_event(&state, did.as_str(), Some(&handle)).await 133 104 { 134 105 warn!( 135 106 "Failed to sequence identity event for admin handle update: {}", 136 107 e 137 108 ); 138 109 } 139 - if let Err(e) = crate::api::identity::did::update_plc_handle(&state, did, &handle).await 110 + if let Err(e) = crate::api::identity::did::update_plc_handle(&state, did.as_str(), &handle).await 140 111 { 141 112 warn!("Failed to update PLC handle for admin handle update: {}", e); 142 113 } 143 - (StatusCode::OK, Json(json!({}))).into_response() 114 + EmptyResponse::ok().into_response() 144 115 } 145 116 Err(e) => { 146 117 error!("DB error updating handle: {:?}", e); 147 - ( 148 - StatusCode::INTERNAL_SERVER_ERROR, 149 - Json(json!({"error": "InternalError"})), 150 - ) 151 - .into_response() 118 + ApiError::InternalError(None).into_response() 152 119 } 153 120 } 154 121 } 155 122 156 123 #[derive(Deserialize)] 157 124 pub struct UpdateAccountPasswordInput { 158 - pub did: String, 159 - pub password: String, 125 + pub did: Did, 126 + pub password: PlainPassword, 160 127 } 161 128 162 129 pub async fn update_account_password( ··· 164 131 _auth: BearerAuthAdmin, 165 132 Json(input): Json<UpdateAccountPasswordInput>, 166 133 ) -> Response { 167 - let did = input.did.trim(); 134 + let did = &input.did; 168 135 let password = input.password.trim(); 169 - if did.is_empty() || password.is_empty() { 170 - return ( 171 - StatusCode::BAD_REQUEST, 172 - Json(json!({"error": "InvalidRequest", "message": "did and password are required"})), 173 - ) 174 - .into_response(); 136 + if password.is_empty() { 137 + return ApiError::InvalidRequest("password is required".into()).into_response(); 175 138 } 176 139 let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) { 177 140 Ok(h) => h, 178 141 Err(e) => { 179 142 error!("Failed to hash password: {:?}", e); 180 - return ( 181 - StatusCode::INTERNAL_SERVER_ERROR, 182 - Json(json!({"error": "InternalError"})), 183 - ) 184 - .into_response(); 143 + return ApiError::InternalError(None).into_response(); 185 144 } 186 145 }; 187 146 let result = sqlx::query!( 188 147 "UPDATE users SET password_hash = $1 WHERE did = $2", 189 148 password_hash, 190 - did 149 + did.as_str() 191 150 ) 192 151 .execute(&state.db) 193 152 .await; 194 153 match result { 195 154 Ok(r) => { 196 155 if r.rows_affected() == 0 { 197 - return ( 198 - StatusCode::NOT_FOUND, 199 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 200 - ) 201 - .into_response(); 156 + return ApiError::AccountNotFound.into_response(); 202 157 } 203 - (StatusCode::OK, Json(json!({}))).into_response() 158 + EmptyResponse::ok().into_response() 204 159 } 205 160 Err(e) => { 206 161 error!("DB error updating password: {:?}", e); 207 - ( 208 - StatusCode::INTERNAL_SERVER_ERROR, 209 - Json(json!({"error": "InternalError"})), 210 - ) 211 - .into_response() 162 + ApiError::InternalError(None).into_response() 212 163 } 213 164 } 214 165 }
+12 -39
src/api/admin/invite.rs
··· 1 + use crate::api::EmptyResponse; 2 + use crate::api::error::ApiError; 1 3 use crate::auth::BearerAuthAdmin; 2 4 use crate::state::AppState; 3 5 use axum::{ ··· 7 9 response::{IntoResponse, Response}, 8 10 }; 9 11 use serde::{Deserialize, Serialize}; 10 - use serde_json::json; 11 12 use tracing::error; 12 13 13 14 #[derive(Deserialize)] ··· 47 48 } 48 49 } 49 50 } 50 - (StatusCode::OK, Json(json!({}))).into_response() 51 + EmptyResponse::ok().into_response() 51 52 } 52 53 53 54 #[derive(Deserialize)] ··· 145 146 Ok(rows) => rows, 146 147 Err(e) => { 147 148 error!("DB error fetching invite codes: {:?}", e); 148 - return ( 149 - StatusCode::INTERNAL_SERVER_ERROR, 150 - Json(json!({"error": "InternalError"})), 151 - ) 152 - .into_response(); 149 + return ApiError::InternalError(None).into_response(); 153 150 } 154 151 }; 155 152 let mut codes = Vec::new(); ··· 220 217 ) -> Response { 221 218 let account = input.account.trim(); 222 219 if account.is_empty() { 223 - return ( 224 - StatusCode::BAD_REQUEST, 225 - Json(json!({"error": "InvalidRequest", "message": "account is required"})), 226 - ) 227 - .into_response(); 220 + return ApiError::InvalidRequest("account is required".into()).into_response(); 228 221 } 229 222 let result = sqlx::query!( 230 223 "UPDATE users SET invites_disabled = TRUE WHERE did = $1", ··· 235 228 match result { 236 229 Ok(r) => { 237 230 if r.rows_affected() == 0 { 238 - return ( 239 - StatusCode::NOT_FOUND, 240 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 241 - ) 242 - .into_response(); 231 + return ApiError::AccountNotFound.into_response(); 243 232 } 244 - (StatusCode::OK, Json(json!({}))).into_response() 233 + EmptyResponse::ok().into_response() 245 234 } 246 235 Err(e) => { 247 236 error!("DB error disabling account invites: {:?}", e); 248 - ( 249 - StatusCode::INTERNAL_SERVER_ERROR, 250 - Json(json!({"error": "InternalError"})), 251 - ) 252 - .into_response() 237 + ApiError::InternalError(None).into_response() 253 238 } 254 239 } 255 240 } ··· 266 251 ) -> Response { 267 252 let account = input.account.trim(); 268 253 if account.is_empty() { 269 - return ( 270 - StatusCode::BAD_REQUEST, 271 - Json(json!({"error": "InvalidRequest", "message": "account is required"})), 272 - ) 273 - .into_response(); 254 + return ApiError::InvalidRequest("account is required".into()).into_response(); 274 255 } 275 256 let result = sqlx::query!( 276 257 "UPDATE users SET invites_disabled = FALSE WHERE did = $1", ··· 281 262 match result { 282 263 Ok(r) => { 283 264 if r.rows_affected() == 0 { 284 - return ( 285 - StatusCode::NOT_FOUND, 286 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 287 - ) 288 - .into_response(); 265 + return ApiError::AccountNotFound.into_response(); 289 266 } 290 - (StatusCode::OK, Json(json!({}))).into_response() 267 + EmptyResponse::ok().into_response() 291 268 } 292 269 Err(e) => { 293 270 error!("DB error enabling account invites: {:?}", e); 294 - ( 295 - StatusCode::INTERNAL_SERVER_ERROR, 296 - Json(json!({"error": "InternalError"})), 297 - ) 298 - .into_response() 271 + ApiError::InternalError(None).into_response() 299 272 } 300 273 } 301 274 }
+31 -79
src/api/admin/status.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::auth::BearerAuthAdmin; 2 3 use crate::state::AppState; 3 4 use axum::{ ··· 37 38 Query(params): Query<GetSubjectStatusParams>, 38 39 ) -> Response { 39 40 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 40 - return ( 41 - StatusCode::BAD_REQUEST, 42 - Json(json!({"error": "InvalidRequest", "message": "Must provide did, uri, or blob"})), 43 - ) 44 - .into_response(); 41 + return ApiError::InvalidRequest("Must provide did, uri, or blob".into()).into_response(); 45 42 } 46 43 if let Some(did) = &params.did { 47 44 let user = sqlx::query!( ··· 74 71 .into_response(); 75 72 } 76 73 Ok(None) => { 77 - return ( 78 - StatusCode::NOT_FOUND, 79 - Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 80 - ) 81 - .into_response(); 74 + return ApiError::SubjectNotFound.into_response(); 82 75 } 83 76 Err(e) => { 84 77 error!("DB error in get_subject_status: {:?}", e); 85 - return ( 86 - StatusCode::INTERNAL_SERVER_ERROR, 87 - Json(json!({"error": "InternalError"})), 88 - ) 89 - .into_response(); 78 + return ApiError::InternalError(None).into_response(); 90 79 } 91 80 } 92 81 } ··· 118 107 .into_response(); 119 108 } 120 109 Ok(None) => { 121 - return ( 122 - StatusCode::NOT_FOUND, 123 - Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 124 - ) 125 - .into_response(); 110 + return ApiError::RecordNotFound.into_response(); 126 111 } 127 112 Err(e) => { 128 113 error!("DB error in get_subject_status: {:?}", e); 129 - return ( 130 - StatusCode::INTERNAL_SERVER_ERROR, 131 - Json(json!({"error": "InternalError"})), 132 - ) 133 - .into_response(); 114 + return ApiError::InternalError(None).into_response(); 134 115 } 135 116 } 136 117 } ··· 138 119 let did = match &params.did { 139 120 Some(d) => d, 140 121 None => { 141 - return ( 142 - StatusCode::BAD_REQUEST, 143 - Json(json!({"error": "InvalidRequest", "message": "Must provide a did to request blob state"})), 122 + return ApiError::InvalidRequest( 123 + "Must provide a did to request blob state".into(), 144 124 ) 145 - .into_response(); 125 + .into_response(); 146 126 } 147 127 }; 148 128 let blob = sqlx::query!( ··· 172 152 .into_response(); 173 153 } 174 154 Ok(None) => { 175 - return ( 176 - StatusCode::NOT_FOUND, 177 - Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 178 - ) 179 - .into_response(); 155 + return ApiError::BlobNotFound(None).into_response(); 180 156 } 181 157 Err(e) => { 182 158 error!("DB error in get_subject_status: {:?}", e); 183 - return ( 184 - StatusCode::INTERNAL_SERVER_ERROR, 185 - Json(json!({"error": "InternalError"})), 186 - ) 187 - .into_response(); 159 + return ApiError::InternalError(None).into_response(); 188 160 } 189 161 } 190 162 } 191 - ( 192 - StatusCode::BAD_REQUEST, 193 - Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})), 194 - ) 195 - .into_response() 163 + ApiError::InvalidRequest("Invalid subject type".into()).into_response() 196 164 } 197 165 198 166 #[derive(Deserialize)] ··· 223 191 Ok(tx) => tx, 224 192 Err(e) => { 225 193 error!("Failed to begin transaction: {:?}", e); 226 - return ( 227 - StatusCode::INTERNAL_SERVER_ERROR, 228 - Json(json!({"error": "InternalError"})), 229 - ) 230 - .into_response(); 194 + return ApiError::InternalError(None).into_response(); 231 195 } 232 196 }; 233 197 if let Some(takedown) = &input.takedown { ··· 245 209 .await 246 210 { 247 211 error!("Failed to update user takedown status for {}: {:?}", did, e); 248 - return ( 249 - StatusCode::INTERNAL_SERVER_ERROR, 250 - Json(json!({"error": "InternalError", "message": "Failed to update takedown status"})), 251 - ) 252 - .into_response(); 212 + return ApiError::InternalError(Some( 213 + "Failed to update takedown status".into(), 214 + )) 215 + .into_response(); 253 216 } 254 217 } 255 218 if let Some(deactivated) = &input.deactivated { ··· 270 233 "Failed to update user deactivation status for {}: {:?}", 271 234 did, e 272 235 ); 273 - return ( 274 - StatusCode::INTERNAL_SERVER_ERROR, 275 - Json(json!({"error": "InternalError", "message": "Failed to update deactivation status"})), 276 - ) 277 - .into_response(); 236 + return ApiError::InternalError(Some( 237 + "Failed to update deactivation status".into(), 238 + )) 239 + .into_response(); 278 240 } 279 241 } 280 242 if let Err(e) = tx.commit().await { 281 243 error!("Failed to commit transaction: {:?}", e); 282 - return ( 283 - StatusCode::INTERNAL_SERVER_ERROR, 284 - Json(json!({"error": "InternalError"})), 285 - ) 286 - .into_response(); 244 + return ApiError::InternalError(None).into_response(); 287 245 } 288 246 if let Some(takedown) = &input.takedown { 289 247 let status = if takedown.applied { ··· 363 321 "Failed to update record takedown status for {}: {:?}", 364 322 uri, e 365 323 ); 366 - return ( 367 - StatusCode::INTERNAL_SERVER_ERROR, 368 - Json(json!({"error": "InternalError", "message": "Failed to update takedown status"})), 369 - ) 370 - .into_response(); 324 + return ApiError::InternalError(Some( 325 + "Failed to update takedown status".into(), 326 + )) 327 + .into_response(); 371 328 } 372 329 } 373 330 return ( ··· 401 358 .await 402 359 { 403 360 error!("Failed to update blob takedown status for {}: {:?}", cid, e); 404 - return ( 405 - StatusCode::INTERNAL_SERVER_ERROR, 406 - Json(json!({"error": "InternalError", "message": "Failed to update takedown status"})), 407 - ) 408 - .into_response(); 361 + return ApiError::InternalError(Some( 362 + "Failed to update takedown status".into(), 363 + )) 364 + .into_response(); 409 365 } 410 366 } 411 367 return ( ··· 423 379 } 424 380 _ => {} 425 381 } 426 - ( 427 - StatusCode::BAD_REQUEST, 428 - Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})), 429 - ) 430 - .into_response() 382 + ApiError::InvalidRequest("Invalid subject type".into()).into_response() 431 383 }
+1 -1
src/api/age_assurance.rs
··· 50 50 } 51 51 }; 52 52 53 - let row = match sqlx::query!("SELECT created_at FROM users WHERE did = $1", auth_user.did) 53 + let row = match sqlx::query!("SELECT created_at FROM users WHERE did = $1", &auth_user.did) 54 54 .fetch_optional(&state.db) 55 55 .await 56 56 {
+41 -165
src/api/backup.rs
··· 1 + use crate::api::error::ApiError; 2 + use crate::api::{EmptyResponse, EnabledResponse}; 1 3 use crate::auth::BearerAuth; 2 4 use crate::scheduled::generate_full_backup; 3 5 use crate::state::AppState; ··· 35 37 pub async fn list_backups(State(state): State<AppState>, auth: BearerAuth) -> Response { 36 38 let user = match sqlx::query!( 37 39 "SELECT id, backup_enabled FROM users WHERE did = $1", 38 - auth.0.did 40 + auth.0.did.as_str() 39 41 ) 40 42 .fetch_optional(&state.db) 41 43 .await 42 44 { 43 45 Ok(Some(u)) => u, 44 46 Ok(None) => { 45 - return ( 46 - StatusCode::NOT_FOUND, 47 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 48 - ) 49 - .into_response(); 47 + return ApiError::AccountNotFound.into_response(); 50 48 } 51 49 Err(e) => { 52 50 error!("DB error fetching user: {:?}", e); 53 - return ( 54 - StatusCode::INTERNAL_SERVER_ERROR, 55 - Json(json!({"error": "InternalError", "message": "Database error"})), 56 - ) 57 - .into_response(); 51 + return ApiError::InternalError(None).into_response(); 58 52 } 59 53 }; 60 54 ··· 73 67 Ok(rows) => rows, 74 68 Err(e) => { 75 69 error!("DB error fetching backups: {:?}", e); 76 - return ( 77 - StatusCode::INTERNAL_SERVER_ERROR, 78 - Json(json!({"error": "InternalError", "message": "Database error"})), 79 - ) 80 - .into_response(); 70 + return ApiError::InternalError(None).into_response(); 81 71 } 82 72 }; 83 73 ··· 116 106 let backup_id = match uuid::Uuid::parse_str(&query.id) { 117 107 Ok(id) => id, 118 108 Err(_) => { 119 - return ( 120 - StatusCode::BAD_REQUEST, 121 - Json(json!({"error": "InvalidRequest", "message": "Invalid backup ID"})), 122 - ) 123 - .into_response(); 109 + return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 124 110 } 125 111 }; 126 112 ··· 132 118 WHERE ab.id = $1 AND u.did = $2 133 119 "#, 134 120 backup_id, 135 - auth.0.did 121 + auth.0.did.as_str() 136 122 ) 137 123 .fetch_optional(&state.db) 138 124 .await 139 125 { 140 126 Ok(Some(b)) => b, 141 127 Ok(None) => { 142 - return ( 143 - StatusCode::NOT_FOUND, 144 - Json(json!({"error": "BackupNotFound", "message": "Backup not found"})), 145 - ) 146 - .into_response(); 128 + return ApiError::BackupNotFound.into_response(); 147 129 } 148 130 Err(e) => { 149 131 error!("DB error fetching backup: {:?}", e); 150 - return ( 151 - StatusCode::INTERNAL_SERVER_ERROR, 152 - Json(json!({"error": "InternalError", "message": "Database error"})), 153 - ) 154 - .into_response(); 132 + return ApiError::InternalError(None).into_response(); 155 133 } 156 134 }; 157 135 158 136 let backup_storage = match state.backup_storage.as_ref() { 159 137 Some(storage) => storage, 160 138 None => { 161 - return ( 162 - StatusCode::SERVICE_UNAVAILABLE, 163 - Json( 164 - json!({"error": "BackupsDisabled", "message": "Backup storage not configured"}), 165 - ), 166 - ) 167 - .into_response(); 139 + return ApiError::BackupsDisabled.into_response(); 168 140 } 169 141 }; 170 142 ··· 172 144 Ok(bytes) => bytes, 173 145 Err(e) => { 174 146 error!("Failed to fetch backup from storage: {:?}", e); 175 - return ( 176 - StatusCode::INTERNAL_SERVER_ERROR, 177 - Json(json!({"error": "InternalError", "message": "Failed to retrieve backup"})), 178 - ) 179 - .into_response(); 147 + return ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response(); 180 148 } 181 149 }; 182 150 ··· 207 175 let backup_storage = match state.backup_storage.as_ref() { 208 176 Some(storage) => storage, 209 177 None => { 210 - return ( 211 - StatusCode::SERVICE_UNAVAILABLE, 212 - Json( 213 - json!({"error": "BackupsDisabled", "message": "Backup storage not configured"}), 214 - ), 215 - ) 216 - .into_response(); 178 + return ApiError::BackupsDisabled.into_response(); 217 179 } 218 180 }; 219 181 ··· 224 186 JOIN repos r ON r.user_id = u.id 225 187 WHERE u.did = $1 226 188 "#, 227 - auth.0.did 189 + auth.0.did.as_str() 228 190 ) 229 191 .fetch_optional(&state.db) 230 192 .await 231 193 { 232 194 Ok(Some(u)) => u, 233 195 Ok(None) => { 234 - return ( 235 - StatusCode::NOT_FOUND, 236 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 237 - ) 238 - .into_response(); 196 + return ApiError::AccountNotFound.into_response(); 239 197 } 240 198 Err(e) => { 241 199 error!("DB error fetching user: {:?}", e); 242 - return ( 243 - StatusCode::INTERNAL_SERVER_ERROR, 244 - Json(json!({"error": "InternalError", "message": "Database error"})), 245 - ) 246 - .into_response(); 200 + return ApiError::InternalError(None).into_response(); 247 201 } 248 202 }; 249 203 250 204 if user.deactivated_at.is_some() { 251 - return ( 252 - StatusCode::BAD_REQUEST, 253 - Json(json!({"error": "AccountDeactivated", "message": "Account is deactivated"})), 254 - ) 255 - .into_response(); 205 + return ApiError::AccountDeactivated.into_response(); 256 206 } 257 207 258 208 let repo_rev = match &user.repo_rev { 259 209 Some(rev) => rev.clone(), 260 210 None => { 261 - return ( 262 - StatusCode::BAD_REQUEST, 263 - Json( 264 - json!({"error": "RepoNotReady", "message": "Repository not ready for backup"}), 265 - ), 266 - ) 267 - .into_response(); 211 + return ApiError::RepoNotReady.into_response(); 268 212 } 269 213 }; 270 214 271 215 let head_cid = match Cid::from_str(&user.repo_root_cid) { 272 216 Ok(c) => c, 273 217 Err(_) => { 274 - return ( 275 - StatusCode::INTERNAL_SERVER_ERROR, 276 - Json(json!({"error": "InternalError", "message": "Invalid repo root CID"})), 277 - ) 278 - .into_response(); 218 + return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); 279 219 } 280 220 }; 281 221 ··· 283 223 Ok(bytes) => bytes, 284 224 Err(e) => { 285 225 error!("Failed to generate CAR: {:?}", e); 286 - return ( 287 - StatusCode::INTERNAL_SERVER_ERROR, 288 - Json(json!({"error": "InternalError", "message": "Failed to generate backup"})), 289 - ) 290 - .into_response(); 226 + return ApiError::InternalError(Some("Failed to generate backup".into())).into_response(); 291 227 } 292 228 }; 293 229 ··· 301 237 Ok(key) => key, 302 238 Err(e) => { 303 239 error!("Failed to upload backup: {:?}", e); 304 - return ( 305 - StatusCode::INTERNAL_SERVER_ERROR, 306 - Json(json!({"error": "InternalError", "message": "Failed to store backup"})), 307 - ) 308 - .into_response(); 240 + return ApiError::InternalError(Some("Failed to store backup".into())).into_response(); 309 241 } 310 242 }; 311 243 ··· 335 267 "Failed to rollback orphaned backup from S3" 336 268 ); 337 269 } 338 - return ( 339 - StatusCode::INTERNAL_SERVER_ERROR, 340 - Json(json!({"error": "InternalError", "message": "Failed to record backup"})), 341 - ) 342 - .into_response(); 270 + return ApiError::InternalError(Some("Failed to record backup".into())).into_response(); 343 271 } 344 272 }; 345 273 ··· 420 348 let backup_id = match uuid::Uuid::parse_str(&query.id) { 421 349 Ok(id) => id, 422 350 Err(_) => { 423 - return ( 424 - StatusCode::BAD_REQUEST, 425 - Json(json!({"error": "InvalidRequest", "message": "Invalid backup ID"})), 426 - ) 427 - .into_response(); 351 + return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 428 352 } 429 353 }; 430 354 ··· 436 360 WHERE ab.id = $1 AND u.did = $2 437 361 "#, 438 362 backup_id, 439 - auth.0.did 363 + auth.0.did.as_str() 440 364 ) 441 365 .fetch_optional(&state.db) 442 366 .await 443 367 { 444 368 Ok(Some(b)) => b, 445 369 Ok(None) => { 446 - return ( 447 - StatusCode::NOT_FOUND, 448 - Json(json!({"error": "BackupNotFound", "message": "Backup not found"})), 449 - ) 450 - .into_response(); 370 + return ApiError::BackupNotFound.into_response(); 451 371 } 452 372 Err(e) => { 453 373 error!("DB error fetching backup: {:?}", e); 454 - return ( 455 - StatusCode::INTERNAL_SERVER_ERROR, 456 - Json(json!({"error": "InternalError", "message": "Database error"})), 457 - ) 458 - .into_response(); 374 + return ApiError::InternalError(None).into_response(); 459 375 } 460 376 }; 461 377 462 378 if backup.deactivated_at.is_some() { 463 - return ( 464 - StatusCode::BAD_REQUEST, 465 - Json(json!({"error": "AccountDeactivated", "message": "Account is deactivated"})), 466 - ) 467 - .into_response(); 379 + return ApiError::AccountDeactivated.into_response(); 468 380 } 469 381 470 382 if let Some(backup_storage) = state.backup_storage.as_ref() ··· 482 394 .await 483 395 { 484 396 error!("DB error deleting backup: {:?}", e); 485 - return ( 486 - StatusCode::INTERNAL_SERVER_ERROR, 487 - Json(json!({"error": "InternalError", "message": "Failed to delete backup"})), 488 - ) 489 - .into_response(); 397 + return ApiError::InternalError(Some("Failed to delete backup".into())).into_response(); 490 398 } 491 399 492 400 info!(did = %auth.0.did, backup_id = %backup_id, "Deleted backup"); 493 401 494 - (StatusCode::OK, Json(json!({}))).into_response() 402 + EmptyResponse::ok().into_response() 495 403 } 496 404 497 405 #[derive(Deserialize)] ··· 507 415 ) -> Response { 508 416 let user = match sqlx::query!( 509 417 "SELECT deactivated_at FROM users WHERE did = $1", 510 - auth.0.did 418 + auth.0.did.as_str() 511 419 ) 512 420 .fetch_optional(&state.db) 513 421 .await 514 422 { 515 423 Ok(Some(u)) => u, 516 424 Ok(None) => { 517 - return ( 518 - StatusCode::NOT_FOUND, 519 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 520 - ) 521 - .into_response(); 425 + return ApiError::AccountNotFound.into_response(); 522 426 } 523 427 Err(e) => { 524 428 error!("DB error fetching user: {:?}", e); 525 - return ( 526 - StatusCode::INTERNAL_SERVER_ERROR, 527 - Json(json!({"error": "InternalError", "message": "Database error"})), 528 - ) 529 - .into_response(); 429 + return ApiError::InternalError(None).into_response(); 530 430 } 531 431 }; 532 432 533 433 if user.deactivated_at.is_some() { 534 - return ( 535 - StatusCode::BAD_REQUEST, 536 - Json(json!({"error": "AccountDeactivated", "message": "Account is deactivated"})), 537 - ) 538 - .into_response(); 434 + return ApiError::AccountDeactivated.into_response(); 539 435 } 540 436 541 437 if let Err(e) = sqlx::query!( 542 438 "UPDATE users SET backup_enabled = $1 WHERE did = $2", 543 439 input.enabled, 544 - auth.0.did 440 + auth.0.did.as_str() 545 441 ) 546 442 .execute(&state.db) 547 443 .await 548 444 { 549 445 error!("DB error updating backup_enabled: {:?}", e); 550 - return ( 551 - StatusCode::INTERNAL_SERVER_ERROR, 552 - Json(json!({"error": "InternalError", "message": "Failed to update setting"})), 553 - ) 554 - .into_response(); 446 + return ApiError::InternalError(Some("Failed to update setting".into())).into_response(); 555 447 } 556 448 557 449 info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting"); 558 450 559 - (StatusCode::OK, Json(json!({"enabled": input.enabled}))).into_response() 451 + EnabledResponse::new(input.enabled).into_response() 560 452 } 561 453 562 454 pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Response { 563 - let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", auth.0.did) 455 + let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", auth.0.did.as_str()) 564 456 .fetch_optional(&state.db) 565 457 .await 566 458 { 567 459 Ok(Some(u)) => u, 568 460 Ok(None) => { 569 - return ( 570 - StatusCode::NOT_FOUND, 571 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 572 - ) 573 - .into_response(); 461 + return ApiError::AccountNotFound.into_response(); 574 462 } 575 463 Err(e) => { 576 464 error!("DB error fetching user: {:?}", e); 577 - return ( 578 - StatusCode::INTERNAL_SERVER_ERROR, 579 - Json(json!({"error": "InternalError", "message": "Database error"})), 580 - ) 581 - .into_response(); 465 + return ApiError::InternalError(None).into_response(); 582 466 } 583 467 }; 584 468 ··· 597 481 Ok(rows) => rows, 598 482 Err(e) => { 599 483 error!("DB error fetching blobs: {:?}", e); 600 - return ( 601 - StatusCode::INTERNAL_SERVER_ERROR, 602 - Json(json!({"error": "InternalError", "message": "Database error"})), 603 - ) 604 - .into_response(); 484 + return ApiError::InternalError(None).into_response(); 605 485 } 606 486 }; 607 487 ··· 695 575 696 576 if let Err(e) = zip.finish() { 697 577 error!("Failed to finish zip: {:?}", e); 698 - return ( 699 - StatusCode::INTERNAL_SERVER_ERROR, 700 - Json(json!({"error": "InternalError", "message": "Failed to create zip file"})), 701 - ) 702 - .into_response(); 578 + return ApiError::InternalError(Some("Failed to create zip file".into())).into_response(); 703 579 } 704 580 } 705 581
+79 -310
src/api/delegation.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::api::repo::record::utils::create_signed_commit; 2 3 use crate::auth::BearerAuth; 3 4 use crate::delegation::{self, DelegationActionType}; 4 5 use crate::oauth::db as oauth_db; 5 6 use crate::state::{AppState, RateLimitKind}; 7 + use crate::types::{Did, Handle}; 6 8 use crate::util::extract_client_ip; 7 - use crate::validation::is_valid_did; 8 9 use axum::{ 9 10 Json, 10 11 extract::{Query, State}, ··· 21 22 #[derive(Debug, Serialize)] 22 23 #[serde(rename_all = "camelCase")] 23 24 pub struct ControllerInfo { 24 - pub did: String, 25 - pub handle: String, 25 + pub did: Did, 26 + pub handle: Handle, 26 27 pub granted_scopes: String, 27 28 pub granted_at: chrono::DateTime<chrono::Utc>, 28 29 pub is_active: bool, ··· 38 39 Ok(c) => c, 39 40 Err(e) => { 40 41 tracing::error!("Failed to list controllers: {:?}", e); 41 - return ( 42 - StatusCode::INTERNAL_SERVER_ERROR, 43 - Json(serde_json::json!({ 44 - "error": "ServerError", 45 - "message": "Failed to list controllers" 46 - })), 47 - ) 48 - .into_response(); 42 + return ApiError::InternalError(Some("Failed to list controllers".into())).into_response(); 49 43 } 50 44 }; 51 45 ··· 53 47 controllers: controllers 54 48 .into_iter() 55 49 .map(|c| ControllerInfo { 56 - did: c.did, 50 + did: c.did.into(), 57 51 handle: c.handle, 58 52 granted_scopes: c.granted_scopes, 59 53 granted_at: c.granted_at, ··· 66 60 67 61 #[derive(Debug, Deserialize)] 68 62 pub struct AddControllerInput { 69 - pub controller_did: String, 63 + pub controller_did: Did, 70 64 pub granted_scopes: String, 71 65 } 72 66 ··· 75 69 auth: BearerAuth, 76 70 Json(input): Json<AddControllerInput>, 77 71 ) -> Response { 78 - if !is_valid_did(&input.controller_did) { 79 - return ( 80 - StatusCode::BAD_REQUEST, 81 - Json(serde_json::json!({ 82 - "error": "InvalidRequest", 83 - "message": "Invalid DID format" 84 - })), 85 - ) 86 - .into_response(); 87 - } 88 - 89 72 if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.granted_scopes) { 90 - return ( 91 - StatusCode::BAD_REQUEST, 92 - Json(serde_json::json!({ 93 - "error": "InvalidScopes", 94 - "message": e 95 - })), 96 - ) 97 - .into_response(); 73 + return ApiError::InvalidScopes(e).into_response(); 98 74 } 99 75 100 76 let controller_exists: bool = sqlx::query_scalar!( 101 77 r#"SELECT EXISTS(SELECT 1 FROM users WHERE did = $1) as "exists!""#, 102 - input.controller_did 78 + input.controller_did.as_str() 103 79 ) 104 80 .fetch_one(&state.db) 105 81 .await 106 82 .unwrap_or(false); 107 83 108 84 if !controller_exists { 109 - return ( 110 - StatusCode::NOT_FOUND, 111 - Json(serde_json::json!({ 112 - "error": "ControllerNotFound", 113 - "message": "Controller account not found" 114 - })), 115 - ) 116 - .into_response(); 85 + return ApiError::ControllerNotFound.into_response(); 117 86 } 118 87 119 88 match delegation::controls_any_accounts(&state.db, &auth.0.did).await { 120 89 Ok(true) => { 121 - return ( 122 - StatusCode::BAD_REQUEST, 123 - Json(serde_json::json!({ 124 - "error": "InvalidDelegation", 125 - "message": "Cannot add controllers to an account that controls other accounts" 126 - })), 90 + return ApiError::InvalidDelegation( 91 + "Cannot add controllers to an account that controls other accounts".into(), 127 92 ) 128 - .into_response(); 93 + .into_response(); 129 94 } 130 95 Err(e) => { 131 96 tracing::error!("Failed to check delegation status: {:?}", e); 132 - return ( 133 - StatusCode::INTERNAL_SERVER_ERROR, 134 - Json(serde_json::json!({ 135 - "error": "ServerError", 136 - "message": "Failed to verify delegation status" 137 - })), 138 - ) 97 + return ApiError::InternalError(Some("Failed to verify delegation status".into())) 139 98 .into_response(); 140 99 } 141 100 Ok(false) => {} ··· 143 102 144 103 match delegation::has_any_controllers(&state.db, &input.controller_did).await { 145 104 Ok(true) => { 146 - return ( 147 - StatusCode::BAD_REQUEST, 148 - Json(serde_json::json!({ 149 - "error": "InvalidDelegation", 150 - "message": "Cannot add a controlled account as a controller" 151 - })), 105 + return ApiError::InvalidDelegation( 106 + "Cannot add a controlled account as a controller".into(), 152 107 ) 153 - .into_response(); 108 + .into_response(); 154 109 } 155 110 Err(e) => { 156 111 tracing::error!("Failed to check controller status: {:?}", e); 157 - return ( 158 - StatusCode::INTERNAL_SERVER_ERROR, 159 - Json(serde_json::json!({ 160 - "error": "ServerError", 161 - "message": "Failed to verify controller status" 162 - })), 163 - ) 112 + return ApiError::InternalError(Some("Failed to verify controller status".into())) 164 113 .into_response(); 165 114 } 166 115 Ok(false) => {} ··· 200 149 } 201 150 Err(e) => { 202 151 tracing::error!("Failed to add controller: {:?}", e); 203 - ( 204 - StatusCode::INTERNAL_SERVER_ERROR, 205 - Json(serde_json::json!({ 206 - "error": "ServerError", 207 - "message": "Failed to add controller" 208 - })), 209 - ) 210 - .into_response() 152 + ApiError::InternalError(Some("Failed to add controller".into())).into_response() 211 153 } 212 154 } 213 155 } 214 156 215 157 #[derive(Debug, Deserialize)] 216 158 pub struct RemoveControllerInput { 217 - pub controller_did: String, 159 + pub controller_did: Did, 218 160 } 219 161 220 162 pub async fn remove_controller( ··· 222 164 auth: BearerAuth, 223 165 Json(input): Json<RemoveControllerInput>, 224 166 ) -> Response { 225 - if !is_valid_did(&input.controller_did) { 226 - return ( 227 - StatusCode::BAD_REQUEST, 228 - Json(serde_json::json!({ 229 - "error": "InvalidRequest", 230 - "message": "Invalid DID format" 231 - })), 232 - ) 233 - .into_response(); 234 - } 235 - 236 167 match delegation::revoke_delegation(&state.db, &auth.0.did, &input.controller_did, &auth.0.did) 237 168 .await 238 169 { ··· 242 173 WHERE user_id = (SELECT id FROM users WHERE did = $1) 243 174 AND created_by_controller_did = $2 244 175 RETURNING id"#, 245 - auth.0.did, 246 - input.controller_did 176 + &auth.0.did, 177 + input.controller_did.as_str() 247 178 ) 248 179 .fetch_all(&state.db) 249 180 .await ··· 281 212 ) 282 213 .into_response() 283 214 } 284 - Ok(false) => ( 285 - StatusCode::NOT_FOUND, 286 - Json(serde_json::json!({ 287 - "error": "DelegationNotFound", 288 - "message": "No active delegation found for this controller" 289 - })), 290 - ) 291 - .into_response(), 215 + Ok(false) => ApiError::DelegationNotFound.into_response(), 292 216 Err(e) => { 293 217 tracing::error!("Failed to remove controller: {:?}", e); 294 - ( 295 - StatusCode::INTERNAL_SERVER_ERROR, 296 - Json(serde_json::json!({ 297 - "error": "ServerError", 298 - "message": "Failed to remove controller" 299 - })), 300 - ) 301 - .into_response() 218 + ApiError::InternalError(Some("Failed to remove controller".into())).into_response() 302 219 } 303 220 } 304 221 } 305 222 306 223 #[derive(Debug, Deserialize)] 307 224 pub struct UpdateControllerScopesInput { 308 - pub controller_did: String, 225 + pub controller_did: Did, 309 226 pub granted_scopes: String, 310 227 } 311 228 ··· 314 231 auth: BearerAuth, 315 232 Json(input): Json<UpdateControllerScopesInput>, 316 233 ) -> Response { 317 - if !is_valid_did(&input.controller_did) { 318 - return ( 319 - StatusCode::BAD_REQUEST, 320 - Json(serde_json::json!({ 321 - "error": "InvalidRequest", 322 - "message": "Invalid DID format" 323 - })), 324 - ) 325 - .into_response(); 326 - } 327 - 328 234 if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.granted_scopes) { 329 - return ( 330 - StatusCode::BAD_REQUEST, 331 - Json(serde_json::json!({ 332 - "error": "InvalidScopes", 333 - "message": e 334 - })), 335 - ) 336 - .into_response(); 235 + return ApiError::InvalidScopes(e).into_response(); 337 236 } 338 237 339 238 match delegation::update_delegation_scopes( ··· 367 266 ) 368 267 .into_response() 369 268 } 370 - Ok(false) => ( 371 - StatusCode::NOT_FOUND, 372 - Json(serde_json::json!({ 373 - "error": "DelegationNotFound", 374 - "message": "No active delegation found for this controller" 375 - })), 376 - ) 377 - .into_response(), 269 + Ok(false) => ApiError::DelegationNotFound.into_response(), 378 270 Err(e) => { 379 271 tracing::error!("Failed to update controller scopes: {:?}", e); 380 - ( 381 - StatusCode::INTERNAL_SERVER_ERROR, 382 - Json(serde_json::json!({ 383 - "error": "ServerError", 384 - "message": "Failed to update controller scopes" 385 - })), 386 - ) 387 - .into_response() 272 + ApiError::InternalError(Some("Failed to update controller scopes".into())).into_response() 388 273 } 389 274 } 390 275 } ··· 392 277 #[derive(Debug, Serialize)] 393 278 #[serde(rename_all = "camelCase")] 394 279 pub struct DelegatedAccountInfo { 395 - pub did: String, 396 - pub handle: String, 280 + pub did: Did, 281 + pub handle: Handle, 397 282 pub granted_scopes: String, 398 283 pub granted_at: chrono::DateTime<chrono::Utc>, 399 284 } ··· 408 293 Ok(a) => a, 409 294 Err(e) => { 410 295 tracing::error!("Failed to list controlled accounts: {:?}", e); 411 - return ( 412 - StatusCode::INTERNAL_SERVER_ERROR, 413 - Json(serde_json::json!({ 414 - "error": "ServerError", 415 - "message": "Failed to list controlled accounts" 416 - })), 417 - ) 296 + return ApiError::InternalError(Some("Failed to list controlled accounts".into())) 418 297 .into_response(); 419 298 } 420 299 }; ··· 423 302 accounts: accounts 424 303 .into_iter() 425 304 .map(|a| DelegatedAccountInfo { 426 - did: a.did, 305 + did: a.did.into(), 427 306 handle: a.handle, 428 307 granted_scopes: a.granted_scopes, 429 308 granted_at: a.granted_at, ··· 449 328 #[serde(rename_all = "camelCase")] 450 329 pub struct AuditLogEntry { 451 330 pub id: String, 452 - pub delegated_did: String, 453 - pub actor_did: String, 454 - pub controller_did: Option<String>, 331 + pub delegated_did: Did, 332 + pub actor_did: Did, 333 + pub controller_did: Option<Did>, 455 334 pub action_type: String, 456 335 pub action_details: Option<serde_json::Value>, 457 336 pub created_at: chrono::DateTime<chrono::Utc>, ··· 478 357 Ok(e) => e, 479 358 Err(e) => { 480 359 tracing::error!("Failed to get audit log: {:?}", e); 481 - return ( 482 - StatusCode::INTERNAL_SERVER_ERROR, 483 - Json(serde_json::json!({ 484 - "error": "ServerError", 485 - "message": "Failed to get audit log" 486 - })), 487 - ) 488 - .into_response(); 360 + return ApiError::InternalError(Some("Failed to get audit log".into())).into_response(); 489 361 } 490 362 }; 491 363 ··· 498 370 .into_iter() 499 371 .map(|e| AuditLogEntry { 500 372 id: e.id.to_string(), 501 - delegated_did: e.delegated_did, 502 - actor_did: e.actor_did, 503 - controller_did: e.controller_did, 373 + delegated_did: e.delegated_did.into(), 374 + actor_did: e.actor_did.into(), 375 + controller_did: e.controller_did.map(Into::into), 504 376 action_type: format!("{:?}", e.action_type), 505 377 action_details: e.action_details, 506 378 created_at: e.created_at, ··· 551 423 #[derive(Debug, Serialize)] 552 424 #[serde(rename_all = "camelCase")] 553 425 pub struct CreateDelegatedAccountResponse { 554 - pub did: String, 555 - pub handle: String, 426 + pub did: Did, 427 + pub handle: Handle, 556 428 } 557 429 558 430 pub async fn create_delegated_account( ··· 567 439 .await 568 440 { 569 441 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 570 - return ( 571 - StatusCode::TOO_MANY_REQUESTS, 572 - Json(json!({ 573 - "error": "RateLimitExceeded", 574 - "message": "Too many account creation attempts. Please try again later." 575 - })), 576 - ) 577 - .into_response(); 442 + return ApiError::RateLimitExceeded(Some( 443 + "Too many account creation attempts. Please try again later.".into(), 444 + )) 445 + .into_response(); 578 446 } 579 447 580 448 if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.controller_scopes) { 581 - return ( 582 - StatusCode::BAD_REQUEST, 583 - Json(json!({ 584 - "error": "InvalidScopes", 585 - "message": e 586 - })), 587 - ) 588 - .into_response(); 449 + return ApiError::InvalidScopes(e).into_response(); 589 450 } 590 451 591 452 match delegation::has_any_controllers(&state.db, &auth.0.did).await { 592 453 Ok(true) => { 593 - return ( 594 - StatusCode::BAD_REQUEST, 595 - Json(json!({ 596 - "error": "InvalidDelegation", 597 - "message": "Cannot create delegated accounts from a controlled account" 598 - })), 454 + return ApiError::InvalidDelegation( 455 + "Cannot create delegated accounts from a controlled account".into(), 599 456 ) 600 - .into_response(); 457 + .into_response(); 601 458 } 602 459 Err(e) => { 603 460 tracing::error!("Failed to check controller status: {:?}", e); 604 - return ( 605 - StatusCode::INTERNAL_SERVER_ERROR, 606 - Json(json!({ 607 - "error": "ServerError", 608 - "message": "Failed to verify controller status" 609 - })), 610 - ) 461 + return ApiError::InternalError(Some("Failed to verify controller status".into())) 611 462 .into_response(); 612 463 } 613 464 Ok(false) => {} ··· 628 479 match crate::api::validation::validate_short_handle(handle_to_validate) { 629 480 Ok(h) => format!("{}.{}", h, hostname), 630 481 Err(e) => { 631 - return ( 632 - StatusCode::BAD_REQUEST, 633 - Json(json!({"error": "InvalidHandle", "message": e.to_string()})), 634 - ) 635 - .into_response(); 482 + return ApiError::InvalidRequest(e.to_string()).into_response(); 636 483 } 637 484 } 638 485 } else { ··· 647 494 if let Some(ref email) = email 648 495 && !crate::api::validation::is_valid_email(email) 649 496 { 650 - return ( 651 - StatusCode::BAD_REQUEST, 652 - Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 653 - ) 654 - .into_response(); 497 + return ApiError::InvalidEmail.into_response(); 655 498 } 656 499 657 500 if let Some(ref code) = input.invite_code { ··· 666 509 .unwrap_or(Some(false)); 667 510 668 511 if valid != Some(true) { 669 - return ( 670 - StatusCode::BAD_REQUEST, 671 - Json(json!({"error": "InvalidInviteCode", "message": "Invalid or expired invite code"})), 672 - ) 673 - .into_response(); 512 + return ApiError::InvalidInviteCode.into_response(); 674 513 } 675 514 } else { 676 515 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 677 516 .map(|v| v == "true" || v == "1") 678 517 .unwrap_or(false); 679 518 if invite_required { 680 - return ( 681 - StatusCode::BAD_REQUEST, 682 - Json(json!({"error": "InviteCodeRequired", "message": "An invite code is required to create an account"})), 683 - ) 684 - .into_response(); 519 + return ApiError::InviteCodeRequired.into_response(); 685 520 } 686 521 } 687 522 ··· 696 531 Ok(k) => k, 697 532 Err(e) => { 698 533 error!("Error creating signing key: {:?}", e); 699 - return ( 700 - StatusCode::INTERNAL_SERVER_ERROR, 701 - Json(json!({"error": "InternalError"})), 702 - ) 703 - .into_response(); 534 + return ApiError::InternalError(None).into_response(); 704 535 } 705 536 }; 706 537 ··· 716 547 Ok(r) => r, 717 548 Err(e) => { 718 549 error!("Error creating PLC genesis operation: {:?}", e); 719 - return ( 720 - StatusCode::INTERNAL_SERVER_ERROR, 721 - Json( 722 - json!({"error": "InternalError", "message": "Failed to create PLC operation"}), 723 - ), 724 - ) 550 + return ApiError::InternalError(Some("Failed to create PLC operation".into())) 725 551 .into_response(); 726 552 } 727 553 }; ··· 732 558 .await 733 559 { 734 560 error!("Failed to submit PLC genesis operation: {:?}", e); 735 - return ( 736 - StatusCode::BAD_GATEWAY, 737 - Json(json!({ 738 - "error": "UpstreamError", 739 - "message": format!("Failed to register DID with PLC directory: {}", e) 740 - })), 741 - ) 742 - .into_response(); 561 + return ApiError::UpstreamErrorMsg(format!( 562 + "Failed to register DID with PLC directory: {}", 563 + e 564 + )) 565 + .into_response(); 743 566 } 744 567 745 568 let did = genesis_result.did; 746 - info!(did = %did, handle = %handle, controller = %auth.0.did, "Created DID for delegated account"); 569 + info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account"); 747 570 748 571 let mut tx = match state.db.begin().await { 749 572 Ok(tx) => tx, 750 573 Err(e) => { 751 574 error!("Error starting transaction: {:?}", e); 752 - return ( 753 - StatusCode::INTERNAL_SERVER_ERROR, 754 - Json(json!({"error": "InternalError"})), 755 - ) 756 - .into_response(); 575 + return ApiError::InternalError(None).into_response(); 757 576 } 758 577 }; 759 578 ··· 777 596 { 778 597 let constraint = db_err.constraint().unwrap_or(""); 779 598 if constraint.contains("handle") { 780 - return ( 781 - StatusCode::BAD_REQUEST, 782 - Json(json!({"error": "HandleNotAvailable", "message": "Handle already taken"})), 783 - ) 784 - .into_response(); 599 + return ApiError::HandleNotAvailable(None).into_response(); 785 600 } else if constraint.contains("email") { 786 - return ( 787 - StatusCode::BAD_REQUEST, 788 - Json( 789 - json!({"error": "InvalidEmail", "message": "Email already registered"}), 790 - ), 791 - ) 792 - .into_response(); 601 + return ApiError::EmailTaken.into_response(); 793 602 } 794 603 } 795 604 error!("Error inserting user: {:?}", e); 796 - return ( 797 - StatusCode::INTERNAL_SERVER_ERROR, 798 - Json(json!({"error": "InternalError"})), 799 - ) 800 - .into_response(); 605 + return ApiError::InternalError(None).into_response(); 801 606 } 802 607 }; 803 608 ··· 805 610 Ok(bytes) => bytes, 806 611 Err(e) => { 807 612 error!("Error encrypting signing key: {:?}", e); 808 - return ( 809 - StatusCode::INTERNAL_SERVER_ERROR, 810 - Json(json!({"error": "InternalError"})), 811 - ) 812 - .into_response(); 613 + return ApiError::InternalError(None).into_response(); 813 614 } 814 615 }; 815 616 ··· 823 624 .await 824 625 { 825 626 error!("Error inserting user key: {:?}", e); 826 - return ( 827 - StatusCode::INTERNAL_SERVER_ERROR, 828 - Json(json!({"error": "InternalError"})), 829 - ) 830 - .into_response(); 627 + return ApiError::InternalError(None).into_response(); 831 628 } 832 629 833 630 if let Err(e) = sqlx::query!( 834 631 r#"INSERT INTO account_delegations (delegated_did, controller_did, granted_scopes, granted_by) 835 632 VALUES ($1, $2, $3, $4)"#, 836 633 did, 837 - auth.0.did, 634 + &auth.0.did, 838 635 input.controller_scopes, 839 - auth.0.did 636 + &auth.0.did 840 637 ) 841 638 .execute(&mut *tx) 842 639 .await 843 640 { 844 641 error!("Error creating initial delegation: {:?}", e); 845 - return ( 846 - StatusCode::INTERNAL_SERVER_ERROR, 847 - Json(json!({"error": "InternalError"})), 848 - ) 849 - .into_response(); 642 + return ApiError::InternalError(None).into_response(); 850 643 } 851 644 852 645 let mst = Mst::new(Arc::new(state.block_store.clone())); ··· 854 647 Ok(c) => c, 855 648 Err(e) => { 856 649 error!("Error persisting MST: {:?}", e); 857 - return ( 858 - StatusCode::INTERNAL_SERVER_ERROR, 859 - Json(json!({"error": "InternalError"})), 860 - ) 861 - .into_response(); 650 + return ApiError::InternalError(None).into_response(); 862 651 } 863 652 }; 864 653 let rev = Tid::now(LimitedU32::MIN); ··· 867 656 Ok(result) => result, 868 657 Err(e) => { 869 658 error!("Error creating genesis commit: {:?}", e); 870 - return ( 871 - StatusCode::INTERNAL_SERVER_ERROR, 872 - Json(json!({"error": "InternalError"})), 873 - ) 874 - .into_response(); 659 + return ApiError::InternalError(None).into_response(); 875 660 } 876 661 }; 877 662 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 878 663 Ok(c) => c, 879 664 Err(e) => { 880 665 error!("Error saving genesis commit: {:?}", e); 881 - return ( 882 - StatusCode::INTERNAL_SERVER_ERROR, 883 - Json(json!({"error": "InternalError"})), 884 - ) 885 - .into_response(); 666 + return ApiError::InternalError(None).into_response(); 886 667 } 887 668 }; 888 669 let commit_cid_str = commit_cid.to_string(); ··· 897 678 .await 898 679 { 899 680 error!("Error inserting repo: {:?}", e); 900 - return ( 901 - StatusCode::INTERNAL_SERVER_ERROR, 902 - Json(json!({"error": "InternalError"})), 903 - ) 904 - .into_response(); 681 + return ApiError::InternalError(None).into_response(); 905 682 } 906 683 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; 907 684 if let Err(e) = sqlx::query!( ··· 917 694 .await 918 695 { 919 696 error!("Error inserting user_blocks: {:?}", e); 920 - return ( 921 - StatusCode::INTERNAL_SERVER_ERROR, 922 - Json(json!({"error": "InternalError"})), 923 - ) 924 - .into_response(); 697 + return ApiError::InternalError(None).into_response(); 925 698 } 926 699 927 700 if let Some(ref code) = input.invite_code { ··· 943 716 944 717 if let Err(e) = tx.commit().await { 945 718 error!("Error committing transaction: {:?}", e); 946 - return ( 947 - StatusCode::INTERNAL_SERVER_ERROR, 948 - Json(json!({"error": "InternalError"})), 949 - ) 950 - .into_response(); 719 + return ApiError::InternalError(None).into_response(); 951 720 } 952 721 953 722 if let Err(e) = ··· 991 760 ) 992 761 .await; 993 762 994 - info!(did = %did, handle = %handle, controller = %auth.0.did, "Delegated account created"); 763 + info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created"); 995 764 996 - Json(CreateDelegatedAccountResponse { did, handle }).into_response() 765 + Json(CreateDelegatedAccountResponse { did: did.into(), handle: handle.into() }).into_response() 997 766 }
+517 -55
src/api/error.rs
··· 1 1 use axum::{ 2 2 Json, 3 + extract::{FromRequest, Request, rejection::JsonRejection}, 3 4 http::StatusCode, 4 5 response::{IntoResponse, Response}, 5 6 }; 6 - use serde::Serialize; 7 + use serde::{Serialize, de::DeserializeOwned}; 7 8 use std::borrow::Cow; 8 9 9 10 #[derive(Debug, Serialize)] ··· 15 16 16 17 #[derive(Debug)] 17 18 pub enum ApiError { 18 - InternalError, 19 + InternalError(Option<String>), 19 20 AuthenticationRequired, 20 - AuthenticationFailed, 21 - AuthenticationFailedMsg(String), 21 + AuthenticationFailed(Option<String>), 22 22 InvalidRequest(String), 23 - InvalidToken, 24 - ExpiredToken, 25 - ExpiredTokenMsg(String), 23 + InvalidToken(Option<String>), 24 + ExpiredToken(Option<String>), 26 25 TokenRequired, 27 26 AccountDeactivated, 28 27 AccountTakedown, 29 28 AccountNotFound, 30 - RepoNotFound, 31 - RepoNotFoundMsg(String), 29 + RepoNotFound(Option<String>), 30 + RepoTakendown, 31 + RepoDeactivated, 32 32 RecordNotFound, 33 - BlobNotFound, 34 - InvalidHandle, 35 - HandleNotAvailable, 33 + BlobNotFound(Option<String>), 34 + InvalidHandle(Option<String>), 35 + HandleNotAvailable(Option<String>), 36 36 HandleTaken, 37 37 InvalidEmail, 38 38 EmailTaken, ··· 40 40 DuplicateCreate, 41 41 DuplicateAppPassword, 42 42 AppPasswordNotFound, 43 - InvalidSwap, 43 + SessionNotFound, 44 + InvalidSwap(Option<String>), 45 + InvalidPassword(String), 46 + InvalidRepo(String), 47 + AccountMigrated, 48 + AccountNotVerified, 49 + InvalidCollection, 50 + InvalidRecord(String), 44 51 Forbidden, 45 - InsufficientScope, 52 + AdminRequired, 53 + InsufficientScope(Option<String>), 46 54 InvitesDisabled, 55 + RateLimitExceeded(Option<String>), 56 + PayloadTooLarge(String), 57 + TotpAlreadyEnabled, 58 + TotpNotEnabled, 59 + InvalidCode(Option<String>), 60 + InvalidChannel, 61 + IdentifierMismatch, 62 + NoPasskeys, 63 + NoChallengeInProgress, 64 + InvalidCredential, 65 + PasskeyCounterAnomaly, 66 + NoRegistrationInProgress, 67 + RegistrationFailed, 68 + PasskeyNotFound, 69 + InvalidId, 70 + InvalidScopes(String), 71 + ControllerNotFound, 72 + InvalidDelegation(String), 73 + DelegationNotFound, 74 + InviteCodeRequired, 75 + BackupNotFound, 76 + BackupsDisabled, 77 + RepoNotReady, 78 + DeviceNotFound, 79 + NoEmail, 80 + MfaVerificationRequired, 81 + AuthorizationError(String), 82 + InvalidDid(String), 83 + InvalidSigningKey, 84 + SetupExpired, 85 + InvalidAccount, 86 + InvalidRecoveryLink, 87 + RecoveryLinkExpired, 88 + MissingEmail, 89 + MissingDiscordId, 90 + MissingTelegramUsername, 91 + MissingSignalNumber, 92 + InvalidVerificationChannel, 93 + SelfHostedDidWebDisabled, 94 + AccountAlreadyExists, 95 + HandleNotFound, 96 + SubjectNotFound, 97 + NotFoundMsg(String), 98 + ServiceUnavailable(Option<String>), 99 + UpstreamErrorMsg(String), 47 100 DatabaseError, 48 101 UpstreamFailure, 49 102 UpstreamTimeout, ··· 58 111 impl ApiError { 59 112 fn status_code(&self) -> StatusCode { 60 113 match self { 61 - Self::InternalError | Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, 62 - Self::UpstreamFailure | Self::UpstreamUnavailable(_) => StatusCode::BAD_GATEWAY, 114 + Self::InternalError(_) | Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, 115 + Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => { 116 + StatusCode::BAD_GATEWAY 117 + } 118 + Self::ServiceUnavailable(_) | Self::BackupsDisabled => { 119 + StatusCode::SERVICE_UNAVAILABLE 120 + } 63 121 Self::UpstreamTimeout => StatusCode::GATEWAY_TIMEOUT, 64 122 Self::UpstreamError { status, .. } => { 65 123 StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY) 66 124 } 67 125 Self::AuthenticationRequired 68 - | Self::AuthenticationFailed 69 - | Self::AuthenticationFailedMsg(_) 70 - | Self::InvalidToken 71 - | Self::ExpiredToken 72 - | Self::ExpiredTokenMsg(_) 73 - | Self::TokenRequired 126 + | Self::AuthenticationFailed(_) 74 127 | Self::AccountDeactivated 75 - | Self::AccountTakedown => StatusCode::UNAUTHORIZED, 76 - Self::Forbidden | Self::InsufficientScope | Self::InvitesDisabled => { 77 - StatusCode::FORBIDDEN 78 - } 128 + | Self::AccountTakedown 129 + | Self::InvalidCode(_) 130 + | Self::InvalidPassword(_) 131 + | Self::PasskeyCounterAnomaly => StatusCode::UNAUTHORIZED, 132 + Self::Forbidden 133 + | Self::AdminRequired 134 + | Self::InsufficientScope(_) 135 + | Self::InvitesDisabled 136 + | Self::InvalidRepo(_) 137 + | Self::AccountMigrated 138 + | Self::AccountNotVerified 139 + | Self::MfaVerificationRequired 140 + | Self::AuthorizationError(_) => StatusCode::FORBIDDEN, 141 + Self::RateLimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS, 142 + Self::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE, 79 143 Self::AccountNotFound 80 - | Self::RepoNotFound 81 - | Self::RepoNotFoundMsg(_) 82 144 | Self::RecordNotFound 83 - | Self::BlobNotFound 84 - | Self::AppPasswordNotFound => StatusCode::NOT_FOUND, 145 + | Self::AppPasswordNotFound 146 + | Self::SessionNotFound 147 + | Self::DeviceNotFound 148 + | Self::ControllerNotFound 149 + | Self::DelegationNotFound 150 + | Self::BackupNotFound 151 + | Self::InvalidRecoveryLink 152 + | Self::HandleNotFound 153 + | Self::SubjectNotFound 154 + | Self::BlobNotFound(_) 155 + | Self::NotFoundMsg(_) => StatusCode::NOT_FOUND, 156 + Self::RepoTakendown 157 + | Self::RepoDeactivated 158 + | Self::RepoNotFound(_) => StatusCode::BAD_REQUEST, 159 + Self::InvalidSwap(_) | Self::TotpAlreadyEnabled => { 160 + StatusCode::CONFLICT 161 + } 85 162 Self::InvalidRequest(_) 86 - | Self::InvalidHandle 87 - | Self::HandleNotAvailable 163 + | Self::InvalidHandle(_) 164 + | Self::HandleNotAvailable(_) 88 165 | Self::HandleTaken 89 166 | Self::InvalidEmail 90 167 | Self::EmailTaken 91 168 | Self::InvalidInviteCode 92 169 | Self::DuplicateCreate 93 170 | Self::DuplicateAppPassword 94 - | Self::InvalidSwap => StatusCode::BAD_REQUEST, 171 + | Self::InvalidCollection 172 + | Self::InvalidRecord(_) 173 + | Self::TotpNotEnabled 174 + | Self::InvalidChannel 175 + | Self::IdentifierMismatch 176 + | Self::NoPasskeys 177 + | Self::NoChallengeInProgress 178 + | Self::InvalidCredential 179 + | Self::NoEmail 180 + | Self::NoRegistrationInProgress 181 + | Self::RegistrationFailed 182 + | Self::InvalidId 183 + | Self::InvalidScopes(_) 184 + | Self::InvalidDelegation(_) 185 + | Self::InviteCodeRequired 186 + | Self::RepoNotReady 187 + | Self::InvalidDid(_) 188 + | Self::InvalidSigningKey 189 + | Self::SetupExpired 190 + | Self::InvalidAccount 191 + | Self::RecoveryLinkExpired 192 + | Self::MissingEmail 193 + | Self::MissingDiscordId 194 + | Self::MissingTelegramUsername 195 + | Self::MissingSignalNumber 196 + | Self::InvalidVerificationChannel 197 + | Self::SelfHostedDidWebDisabled 198 + | Self::AccountAlreadyExists 199 + | Self::InvalidToken(_) 200 + | Self::ExpiredToken(_) 201 + | Self::TokenRequired => StatusCode::BAD_REQUEST, 202 + Self::PasskeyNotFound => StatusCode::NOT_FOUND, 95 203 } 96 204 } 97 205 fn error_name(&self) -> Cow<'static, str> { 98 206 match self { 99 - Self::InternalError | Self::DatabaseError => Cow::Borrowed("InternalError"), 100 - Self::UpstreamFailure | Self::UpstreamUnavailable(_) => { 101 - Cow::Borrowed("UpstreamFailure") 207 + Self::InternalError(_) | Self::DatabaseError => Cow::Borrowed("InternalError"), 208 + Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => { 209 + Cow::Borrowed("UpstreamError") 102 210 } 211 + Self::ServiceUnavailable(_) => Cow::Borrowed("ServiceUnavailable"), 212 + Self::NotFoundMsg(_) => Cow::Borrowed("NotFound"), 103 213 Self::UpstreamTimeout => Cow::Borrowed("UpstreamTimeout"), 104 214 Self::UpstreamError { error, .. } => { 105 215 if let Some(e) = error { ··· 108 218 Cow::Borrowed("UpstreamError") 109 219 } 110 220 Self::AuthenticationRequired => Cow::Borrowed("AuthenticationRequired"), 111 - Self::AuthenticationFailed | Self::AuthenticationFailedMsg(_) => { 112 - Cow::Borrowed("AuthenticationFailed") 113 - } 114 - Self::InvalidToken => Cow::Borrowed("InvalidToken"), 115 - Self::ExpiredToken | Self::ExpiredTokenMsg(_) => Cow::Borrowed("ExpiredToken"), 221 + Self::AuthenticationFailed(_) => Cow::Borrowed("AuthenticationFailed"), 222 + Self::InvalidToken(_) => Cow::Borrowed("InvalidToken"), 223 + Self::ExpiredToken(_) => Cow::Borrowed("ExpiredToken"), 116 224 Self::TokenRequired => Cow::Borrowed("TokenRequired"), 117 225 Self::AccountDeactivated => Cow::Borrowed("AccountDeactivated"), 118 226 Self::AccountTakedown => Cow::Borrowed("AccountTakedown"), 119 227 Self::Forbidden => Cow::Borrowed("Forbidden"), 120 - Self::InsufficientScope => Cow::Borrowed("InsufficientScope"), 228 + Self::AdminRequired => Cow::Borrowed("AdminRequired"), 229 + Self::InsufficientScope(_) => Cow::Borrowed("InsufficientScope"), 121 230 Self::InvitesDisabled => Cow::Borrowed("InvitesDisabled"), 122 231 Self::AccountNotFound => Cow::Borrowed("AccountNotFound"), 123 - Self::RepoNotFound | Self::RepoNotFoundMsg(_) => Cow::Borrowed("RepoNotFound"), 232 + Self::RepoNotFound(_) => Cow::Borrowed("RepoNotFound"), 233 + Self::RepoTakendown => Cow::Borrowed("RepoTakendown"), 234 + Self::RepoDeactivated => Cow::Borrowed("RepoDeactivated"), 124 235 Self::RecordNotFound => Cow::Borrowed("RecordNotFound"), 125 - Self::BlobNotFound => Cow::Borrowed("BlobNotFound"), 236 + Self::BlobNotFound(_) => Cow::Borrowed("BlobNotFound"), 126 237 Self::AppPasswordNotFound => Cow::Borrowed("AppPasswordNotFound"), 238 + Self::SessionNotFound => Cow::Borrowed("SessionNotFound"), 127 239 Self::InvalidRequest(_) => Cow::Borrowed("InvalidRequest"), 128 - Self::InvalidHandle => Cow::Borrowed("InvalidHandle"), 129 - Self::HandleNotAvailable => Cow::Borrowed("HandleNotAvailable"), 240 + Self::InvalidHandle(_) => Cow::Borrowed("InvalidHandle"), 241 + Self::HandleNotAvailable(_) => Cow::Borrowed("HandleNotAvailable"), 130 242 Self::HandleTaken => Cow::Borrowed("HandleTaken"), 131 243 Self::InvalidEmail => Cow::Borrowed("InvalidEmail"), 132 244 Self::EmailTaken => Cow::Borrowed("EmailTaken"), 133 245 Self::InvalidInviteCode => Cow::Borrowed("InvalidInviteCode"), 134 246 Self::DuplicateCreate => Cow::Borrowed("DuplicateCreate"), 135 247 Self::DuplicateAppPassword => Cow::Borrowed("DuplicateAppPassword"), 136 - Self::InvalidSwap => Cow::Borrowed("InvalidSwap"), 248 + Self::InvalidSwap(_) => Cow::Borrowed("InvalidSwap"), 249 + Self::InvalidPassword(_) => Cow::Borrowed("InvalidPassword"), 250 + Self::InvalidRepo(_) => Cow::Borrowed("InvalidRepo"), 251 + Self::AccountMigrated => Cow::Borrowed("AccountMigrated"), 252 + Self::AccountNotVerified => Cow::Borrowed("AccountNotVerified"), 253 + Self::InvalidCollection => Cow::Borrowed("InvalidCollection"), 254 + Self::InvalidRecord(_) => Cow::Borrowed("InvalidRecord"), 255 + Self::TotpAlreadyEnabled => Cow::Borrowed("TotpAlreadyEnabled"), 256 + Self::TotpNotEnabled => Cow::Borrowed("TotpNotEnabled"), 257 + Self::InvalidCode(_) => Cow::Borrowed("InvalidCode"), 258 + Self::InvalidChannel => Cow::Borrowed("InvalidChannel"), 259 + Self::IdentifierMismatch => Cow::Borrowed("IdentifierMismatch"), 260 + Self::NoPasskeys => Cow::Borrowed("NoPasskeys"), 261 + Self::NoChallengeInProgress => Cow::Borrowed("NoChallengeInProgress"), 262 + Self::InvalidCredential => Cow::Borrowed("InvalidCredential"), 263 + Self::PasskeyCounterAnomaly => Cow::Borrowed("PasskeyCounterAnomaly"), 264 + Self::NoRegistrationInProgress => Cow::Borrowed("NoRegistrationInProgress"), 265 + Self::RegistrationFailed => Cow::Borrowed("RegistrationFailed"), 266 + Self::PasskeyNotFound => Cow::Borrowed("PasskeyNotFound"), 267 + Self::InvalidId => Cow::Borrowed("InvalidId"), 268 + Self::InvalidScopes(_) => Cow::Borrowed("InvalidScopes"), 269 + Self::ControllerNotFound => Cow::Borrowed("ControllerNotFound"), 270 + Self::InvalidDelegation(_) => Cow::Borrowed("InvalidDelegation"), 271 + Self::DelegationNotFound => Cow::Borrowed("DelegationNotFound"), 272 + Self::InviteCodeRequired => Cow::Borrowed("InviteCodeRequired"), 273 + Self::BackupNotFound => Cow::Borrowed("BackupNotFound"), 274 + Self::BackupsDisabled => Cow::Borrowed("BackupsDisabled"), 275 + Self::RepoNotReady => Cow::Borrowed("RepoNotReady"), 276 + Self::MfaVerificationRequired => Cow::Borrowed("MfaVerificationRequired"), 277 + Self::RateLimitExceeded(_) => Cow::Borrowed("RateLimitExceeded"), 278 + Self::PayloadTooLarge(_) => Cow::Borrowed("PayloadTooLarge"), 279 + Self::DeviceNotFound => Cow::Borrowed("DeviceNotFound"), 280 + Self::NoEmail => Cow::Borrowed("NoEmail"), 281 + Self::AuthorizationError(_) => Cow::Borrowed("AuthorizationError"), 282 + Self::InvalidDid(_) => Cow::Borrowed("InvalidDid"), 283 + Self::InvalidSigningKey => Cow::Borrowed("InvalidSigningKey"), 284 + Self::SetupExpired => Cow::Borrowed("SetupExpired"), 285 + Self::InvalidAccount => Cow::Borrowed("InvalidAccount"), 286 + Self::InvalidRecoveryLink => Cow::Borrowed("InvalidRecoveryLink"), 287 + Self::RecoveryLinkExpired => Cow::Borrowed("RecoveryLinkExpired"), 288 + Self::MissingEmail => Cow::Borrowed("MissingEmail"), 289 + Self::MissingDiscordId => Cow::Borrowed("MissingDiscordId"), 290 + Self::MissingTelegramUsername => Cow::Borrowed("MissingTelegramUsername"), 291 + Self::MissingSignalNumber => Cow::Borrowed("MissingSignalNumber"), 292 + Self::InvalidVerificationChannel => Cow::Borrowed("InvalidVerificationChannel"), 293 + Self::SelfHostedDidWebDisabled => Cow::Borrowed("SelfHostedDidWebDisabled"), 294 + Self::AccountAlreadyExists => Cow::Borrowed("AccountAlreadyExists"), 295 + Self::HandleNotFound => Cow::Borrowed("HandleNotFound"), 296 + Self::SubjectNotFound => Cow::Borrowed("SubjectNotFound"), 137 297 } 138 298 } 139 299 fn message(&self) -> Option<String> { 140 300 match self { 141 - Self::AuthenticationFailedMsg(msg) 142 - | Self::ExpiredTokenMsg(msg) 143 - | Self::InvalidRequest(msg) 144 - | Self::RepoNotFoundMsg(msg) 145 - | Self::UpstreamUnavailable(msg) => Some(msg.clone()), 301 + Self::InternalError(msg) 302 + | Self::AuthenticationFailed(msg) 303 + | Self::InvalidToken(msg) 304 + | Self::ExpiredToken(msg) 305 + | Self::RepoNotFound(msg) 306 + | Self::BlobNotFound(msg) 307 + | Self::InvalidHandle(msg) 308 + | Self::HandleNotAvailable(msg) 309 + | Self::InvalidSwap(msg) 310 + | Self::InsufficientScope(msg) 311 + | Self::InvalidCode(msg) 312 + | Self::RateLimitExceeded(msg) 313 + | Self::ServiceUnavailable(msg) => msg.clone(), 314 + Self::InvalidRequest(msg) 315 + | Self::UpstreamUnavailable(msg) 316 + | Self::InvalidPassword(msg) 317 + | Self::InvalidRepo(msg) 318 + | Self::InvalidRecord(msg) 319 + | Self::NotFoundMsg(msg) 320 + | Self::UpstreamErrorMsg(msg) 321 + | Self::PayloadTooLarge(msg) => Some(msg.clone()), 322 + Self::AccountMigrated => Some( 323 + "Account has been migrated to another PDS. Repo operations are not allowed." 324 + .to_string(), 325 + ), 326 + Self::AccountNotVerified => Some( 327 + "You must verify at least one notification channel before creating records" 328 + .to_string(), 329 + ), 330 + Self::NoPasskeys => { 331 + Some("No passkeys registered for this account".to_string()) 332 + } 333 + Self::NoChallengeInProgress => Some( 334 + "No passkey authentication in progress or challenge expired".to_string(), 335 + ), 336 + Self::InvalidCredential => Some("Failed to parse credential response".to_string()), 337 + Self::NoRegistrationInProgress => Some( 338 + "No registration in progress. Call startPasskeyRegistration first.".to_string(), 339 + ), 340 + Self::RegistrationFailed => { 341 + Some("Failed to verify passkey registration".to_string()) 342 + } 343 + Self::PasskeyNotFound => Some("Passkey not found".to_string()), 344 + Self::InvalidId => Some("Invalid ID format".to_string()), 345 + Self::InvalidScopes(msg) | Self::InvalidDelegation(msg) => Some(msg.clone()), 346 + Self::ControllerNotFound => Some("Controller account not found".to_string()), 347 + Self::DelegationNotFound => { 348 + Some("No active delegation found for this controller".to_string()) 349 + } 350 + Self::InviteCodeRequired => { 351 + Some("An invite code is required to create an account".to_string()) 352 + } 353 + Self::BackupNotFound => Some("Backup not found".to_string()), 354 + Self::BackupsDisabled => Some("Backup storage not configured".to_string()), 355 + Self::RepoNotReady => Some("Repository not ready for backup".to_string()), 356 + Self::PasskeyCounterAnomaly => Some( 357 + "Authentication failed: security key counter anomaly detected. This may indicate a cloned key.".to_string(), 358 + ), 359 + Self::MfaVerificationRequired => Some( 360 + "This sensitive operation requires MFA verification".to_string(), 361 + ), 362 + Self::DeviceNotFound => Some("Device not found".to_string()), 363 + Self::NoEmail => Some("Recipient has no email address".to_string()), 364 + Self::AuthorizationError(msg) | Self::InvalidDid(msg) => Some(msg.clone()), 365 + Self::InvalidSigningKey => { 366 + Some("Signing key not found, already used, or expired".to_string()) 367 + } 368 + Self::SetupExpired => { 369 + Some("Setup has already been completed or expired".to_string()) 370 + } 371 + Self::InvalidAccount => { 372 + Some("This account is not a passkey-only account".to_string()) 373 + } 374 + Self::InvalidRecoveryLink => Some("Invalid recovery link".to_string()), 375 + Self::RecoveryLinkExpired => Some("Recovery link has expired".to_string()), 376 + Self::MissingEmail => { 377 + Some("Email is required when using email verification".to_string()) 378 + } 379 + Self::MissingDiscordId => { 380 + Some("Discord ID is required when using Discord verification".to_string()) 381 + } 382 + Self::MissingTelegramUsername => { 383 + Some("Telegram username is required when using Telegram verification".to_string()) 384 + } 385 + Self::MissingSignalNumber => { 386 + Some("Signal phone number is required when using Signal verification".to_string()) 387 + } 388 + Self::InvalidVerificationChannel => Some("Invalid verification channel".to_string()), 389 + Self::SelfHostedDidWebDisabled => { 390 + Some("Self-hosted did:web accounts are disabled on this server".to_string()) 391 + } 392 + Self::AccountAlreadyExists => Some("Account already exists".to_string()), 393 + Self::HandleNotFound => Some("Unable to resolve handle".to_string()), 394 + Self::SubjectNotFound => Some("Subject not found".to_string()), 395 + Self::IdentifierMismatch => { 396 + Some("The identifier does not match the verification token".to_string()) 397 + } 146 398 Self::UpstreamError { message, .. } => message.clone(), 147 399 Self::UpstreamTimeout => Some("Upstream service timed out".to_string()), 400 + Self::AdminRequired => Some("This action requires admin privileges".to_string()), 148 401 _ => None, 149 402 } 150 403 } ··· 181 434 (self.status_code(), Json(body)).into_response() 182 435 } 183 436 } 437 + 184 438 185 439 impl From<sqlx::Error> for ApiError { 186 440 fn from(e: sqlx::Error) -> Self { ··· 194 448 match e { 195 449 crate::auth::TokenValidationError::AccountDeactivated => Self::AccountDeactivated, 196 450 crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown, 197 - crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError, 198 - crate::auth::TokenValidationError::AuthenticationFailed => Self::AuthenticationFailed, 199 - crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken, 451 + crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError(None), 452 + crate::auth::TokenValidationError::AuthenticationFailed => { 453 + Self::AuthenticationFailed(None) 454 + } 455 + crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken(None), 200 456 } 201 457 } 202 458 } ··· 212 468 } 213 469 } 214 470 } 471 + 472 + impl From<crate::auth::extractor::AuthError> for ApiError { 473 + fn from(e: crate::auth::extractor::AuthError) -> Self { 474 + match e { 475 + crate::auth::extractor::AuthError::MissingToken => Self::AuthenticationRequired, 476 + crate::auth::extractor::AuthError::InvalidFormat => { 477 + Self::AuthenticationFailed(Some("Invalid authorization header format".to_string())) 478 + } 479 + crate::auth::extractor::AuthError::AuthenticationFailed => { 480 + Self::AuthenticationFailed(None) 481 + } 482 + crate::auth::extractor::AuthError::TokenExpired => { 483 + Self::AuthenticationFailed(Some("Token has expired".to_string())) 484 + } 485 + crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, 486 + crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, 487 + crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired, 488 + } 489 + } 490 + } 491 + 492 + impl From<crate::handle::HandleResolutionError> for ApiError { 493 + fn from(e: crate::handle::HandleResolutionError) -> Self { 494 + match e { 495 + crate::handle::HandleResolutionError::NotFound => Self::HandleNotFound, 496 + crate::handle::HandleResolutionError::InvalidDid => { 497 + Self::InvalidHandle(Some("Invalid DID format in handle record".to_string())) 498 + } 499 + crate::handle::HandleResolutionError::DidMismatch { expected, actual } => { 500 + Self::InvalidHandle(Some(format!( 501 + "Handle DID mismatch: expected {}, got {}", 502 + expected, actual 503 + ))) 504 + } 505 + crate::handle::HandleResolutionError::DnsError(msg) => { 506 + Self::InternalError(Some(format!("DNS resolution failed: {}", msg))) 507 + } 508 + crate::handle::HandleResolutionError::HttpError(msg) => { 509 + Self::InternalError(Some(format!("Handle HTTP resolution failed: {}", msg))) 510 + } 511 + } 512 + } 513 + } 514 + 515 + impl From<crate::auth::verification_token::VerifyError> for ApiError { 516 + fn from(e: crate::auth::verification_token::VerifyError) -> Self { 517 + use crate::auth::verification_token::VerifyError; 518 + match e { 519 + VerifyError::InvalidFormat => { 520 + Self::InvalidRequest("The verification code is invalid or malformed".to_string()) 521 + } 522 + VerifyError::UnsupportedVersion => { 523 + Self::InvalidRequest("This verification code version is not supported".to_string()) 524 + } 525 + VerifyError::Expired => { 526 + Self::InvalidRequest("The verification code has expired. Please request a new one.".to_string()) 527 + } 528 + VerifyError::InvalidSignature => { 529 + Self::InvalidRequest("The verification code is invalid".to_string()) 530 + } 531 + VerifyError::IdentifierMismatch => Self::IdentifierMismatch, 532 + VerifyError::PurposeMismatch => { 533 + Self::InvalidRequest("Verification code purpose does not match".to_string()) 534 + } 535 + VerifyError::ChannelMismatch => { 536 + Self::InvalidRequest("Verification code channel does not match".to_string()) 537 + } 538 + } 539 + } 540 + } 541 + 542 + impl From<crate::api::validation::HandleValidationError> for ApiError { 543 + fn from(e: crate::api::validation::HandleValidationError) -> Self { 544 + use crate::api::validation::HandleValidationError; 545 + match e { 546 + HandleValidationError::Reserved => Self::HandleNotAvailable(None), 547 + HandleValidationError::BannedWord => { 548 + Self::InvalidHandle(Some("Inappropriate language in handle".to_string())) 549 + } 550 + _ => Self::InvalidHandle(Some(e.to_string())), 551 + } 552 + } 553 + } 554 + 555 + impl From<jacquard::types::string::AtStrError> for ApiError { 556 + fn from(e: jacquard::types::string::AtStrError) -> Self { 557 + Self::InvalidRequest(format!("Invalid {}: {}", e.spec, e.kind)) 558 + } 559 + } 560 + 561 + impl From<crate::plc::PlcError> for ApiError { 562 + fn from(e: crate::plc::PlcError) -> Self { 563 + use crate::plc::PlcError; 564 + match e { 565 + PlcError::NotFound => Self::NotFoundMsg("DID not found in PLC directory".into()), 566 + PlcError::Tombstoned => Self::InvalidRequest("DID is tombstoned".into()), 567 + PlcError::Timeout => Self::UpstreamTimeout, 568 + PlcError::CircuitBreakerOpen => { 569 + Self::ServiceUnavailable(Some("PLC directory service temporarily unavailable".into())) 570 + } 571 + PlcError::Http(err) => { 572 + tracing::error!("PLC HTTP error: {:?}", err); 573 + Self::UpstreamErrorMsg("Failed to communicate with PLC directory".into()) 574 + } 575 + PlcError::InvalidResponse(msg) => { 576 + tracing::error!("PLC invalid response: {}", msg); 577 + Self::UpstreamErrorMsg(format!("Invalid response from PLC directory: {}", msg)) 578 + } 579 + PlcError::Serialization(msg) => { 580 + tracing::error!("PLC serialization error: {}", msg); 581 + Self::InternalError(Some(format!("PLC serialization error: {}", msg))) 582 + } 583 + PlcError::Signing(msg) => { 584 + tracing::error!("PLC signing error: {}", msg); 585 + Self::InternalError(Some(format!("PLC signing error: {}", msg))) 586 + } 587 + } 588 + } 589 + } 590 + 591 + impl From<bcrypt::BcryptError> for ApiError { 592 + fn from(e: bcrypt::BcryptError) -> Self { 593 + tracing::error!("Bcrypt error: {:?}", e); 594 + Self::InternalError(None) 595 + } 596 + } 597 + 598 + impl From<cid::Error> for ApiError { 599 + fn from(e: cid::Error) -> Self { 600 + Self::InvalidRequest(format!("Invalid CID: {}", e)) 601 + } 602 + } 603 + 604 + impl From<crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>> for ApiError { 605 + fn from(e: crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>) -> Self { 606 + use crate::circuit_breaker::CircuitBreakerError; 607 + match e { 608 + CircuitBreakerError::CircuitOpen(err) => { 609 + tracing::warn!("PLC directory circuit breaker open: {}", err); 610 + Self::ServiceUnavailable(Some( 611 + "PLC directory service temporarily unavailable".into(), 612 + )) 613 + } 614 + CircuitBreakerError::OperationFailed(plc_err) => Self::from(plc_err), 615 + } 616 + } 617 + } 618 + 619 + impl From<crate::storage::StorageError> for ApiError { 620 + fn from(e: crate::storage::StorageError) -> Self { 621 + tracing::error!("Storage error: {:?}", e); 622 + Self::InternalError(Some("Storage operation failed".into())) 623 + } 624 + } 625 + 626 + pub struct AtpJson<T>(pub T); 627 + 628 + impl<T, S> FromRequest<S> for AtpJson<T> 629 + where 630 + T: DeserializeOwned, 631 + S: Send + Sync, 632 + { 633 + type Rejection = (StatusCode, Json<serde_json::Value>); 634 + 635 + async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> { 636 + match Json::<T>::from_request(req, state).await { 637 + Ok(Json(value)) => Ok(AtpJson(value)), 638 + Err(rejection) => { 639 + let message = extract_json_error_message(&rejection); 640 + Err(( 641 + StatusCode::BAD_REQUEST, 642 + Json(serde_json::json!({ 643 + "error": "InvalidRequest", 644 + "message": message 645 + })), 646 + )) 647 + } 648 + } 649 + } 650 + } 651 + 652 + fn extract_json_error_message(rejection: &JsonRejection) -> String { 653 + match rejection { 654 + JsonRejection::JsonDataError(e) => { 655 + let inner = e.body_text(); 656 + if inner.contains("missing field") { 657 + let field = inner 658 + .split("missing field `") 659 + .nth(1) 660 + .and_then(|s| s.split('`').next()) 661 + .unwrap_or("unknown"); 662 + format!("Missing required field: {}", field) 663 + } else if inner.contains("invalid type") { 664 + format!("Invalid field type: {}", inner) 665 + } else { 666 + inner 667 + } 668 + } 669 + JsonRejection::JsonSyntaxError(_) => "Invalid JSON syntax".to_string(), 670 + JsonRejection::MissingJsonContentType(_) => { 671 + "Content-Type must be application/json".to_string() 672 + } 673 + JsonRejection::BytesRejection(_) => "Failed to read request body".to_string(), 674 + _ => "Invalid request body".to_string(), 675 + } 676 + }
+108 -353
src/api/identity/account.rs
··· 1 1 use super::did::verify_did_web; 2 + use crate::api::error::ApiError; 2 3 use crate::api::repo::record::utils::create_signed_commit; 3 4 use crate::auth::{ServiceTokenVerifier, extract_bearer_token_from_header, is_service_token}; 4 5 use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key}; 5 6 use crate::state::{AppState, RateLimitKind}; 7 + use crate::types::{Did, Handle, PlainPassword}; 6 8 use crate::validation::validate_password; 7 9 use axum::{ 8 10 Json, ··· 10 12 http::{HeaderMap, StatusCode}, 11 13 response::{IntoResponse, Response}, 12 14 }; 15 + use serde_json::json; 13 16 use bcrypt::{DEFAULT_COST, hash}; 14 17 use jacquard::types::{integer::LimitedU32, string::Tid}; 15 18 use jacquard_repo::{mst::Mst, storage::BlockStore}; 16 19 use k256::{SecretKey, ecdsa::SigningKey}; 17 20 use rand::rngs::OsRng; 18 21 use serde::{Deserialize, Serialize}; 19 - use serde_json::json; 20 22 use std::sync::Arc; 21 23 use tracing::{debug, error, info, warn}; 22 24 ··· 40 42 pub struct CreateAccountInput { 41 43 pub handle: String, 42 44 pub email: Option<String>, 43 - pub password: String, 45 + pub password: PlainPassword, 44 46 pub invite_code: Option<String>, 45 47 pub did: Option<String>, 46 48 pub did_type: Option<String>, ··· 54 56 #[derive(Serialize)] 55 57 #[serde(rename_all = "camelCase")] 56 58 pub struct CreateAccountOutput { 57 - pub handle: String, 58 - pub did: String, 59 + pub handle: Handle, 60 + pub did: Did, 59 61 #[serde(skip_serializing_if = "Option::is_none")] 60 62 pub did_doc: Option<serde_json::Value>, 61 63 pub access_jwt: String, ··· 88 90 .await 89 91 { 90 92 warn!(ip = %client_ip, "Account creation rate limit exceeded"); 91 - return ( 92 - StatusCode::TOO_MANY_REQUESTS, 93 - Json(json!({ 94 - "error": "RateLimitExceeded", 95 - "message": "Too many account creation attempts. Please try again later." 96 - })), 97 - ) 98 - .into_response(); 93 + return ApiError::RateLimitExceeded(Some("Too many account creation attempts. Please try again later.".into(),)) 94 + .into_response(); 99 95 } 100 96 101 97 let migration_auth = if let Some(token) = ··· 113 109 } 114 110 Err(e) => { 115 111 error!("Service token verification failed: {:?}", e); 116 - return ( 117 - StatusCode::UNAUTHORIZED, 118 - Json(json!({ 119 - "error": "AuthenticationFailed", 120 - "message": format!("Service token verification failed: {}", e) 121 - })), 122 - ) 123 - .into_response(); 112 + return ApiError::AuthenticationFailed(Some(format!( 113 + "Service token verification failed: {}", 114 + e 115 + ))) 116 + .into_response(); 124 117 } 125 118 } 126 119 } else { ··· 152 145 "[MIGRATION] createAccount: Service token mismatch - token_did={} provided_did={}", 153 146 auth_did, provided_did 154 147 ); 155 - return ( 156 - StatusCode::FORBIDDEN, 157 - Json(json!({ 158 - "error": "AuthorizationError", 159 - "message": format!("Service token issuer {} does not match DID {}", auth_did, provided_did) 160 - })), 161 - ) 162 - .into_response(); 148 + return ApiError::AuthorizationError(format!( 149 + "Service token issuer {} does not match DID {}", 150 + auth_did, provided_did 151 + )) 152 + .into_response(); 163 153 } 164 154 if is_did_web_byod { 165 155 info!(did = %provided_did, "Processing did:web BYOD account creation"); ··· 188 178 }; 189 179 match crate::api::validation::validate_short_handle(handle_to_validate) { 190 180 Ok(h) => h, 191 - Err(crate::api::validation::HandleValidationError::Reserved) => { 192 - return ( 193 - StatusCode::BAD_REQUEST, 194 - Json(json!({"error": "HandleNotAvailable", "message": "Reserved handle"})), 195 - ) 196 - .into_response(); 197 - } 198 181 Err(e) => { 199 - return ( 200 - StatusCode::BAD_REQUEST, 201 - Json(json!({"error": "InvalidHandle", "message": e.to_string()})), 202 - ) 203 - .into_response(); 182 + return ApiError::from(e).into_response(); 204 183 } 205 184 } 206 185 } else { 207 186 if input.handle.contains(' ') || input.handle.contains('\t') { 208 - return ( 209 - StatusCode::BAD_REQUEST, 210 - Json(json!({"error": "InvalidHandle", "message": "Handle cannot contain spaces"})), 211 - ) 212 - .into_response(); 187 + return ApiError::InvalidRequest("Handle cannot contain spaces".into()).into_response(); 213 188 } 214 189 for c in input.handle.chars() { 215 190 if !c.is_ascii_alphanumeric() && c != '.' && c != '-' { 216 - return ( 217 - StatusCode::BAD_REQUEST, 218 - Json(json!({"error": "InvalidHandle", "message": format!("Handle contains invalid character: {}", c)})), 219 - ) 220 - .into_response(); 191 + return ApiError::InvalidRequest(format!( 192 + "Handle contains invalid character: {}", 193 + c 194 + )) 195 + .into_response(); 221 196 } 222 197 } 223 198 let handle_lower = input.handle.to_lowercase(); 224 199 if crate::moderation::has_explicit_slur(&handle_lower) { 225 - return ( 226 - StatusCode::BAD_REQUEST, 227 - Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})), 228 - ) 200 + return ApiError::InvalidRequest("Inappropriate language in handle".into()) 229 201 .into_response(); 230 202 } 231 203 handle_lower ··· 238 210 if let Some(ref email) = email 239 211 && !crate::api::validation::is_valid_email(email) 240 212 { 241 - return ( 242 - StatusCode::BAD_REQUEST, 243 - Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 244 - ) 245 - .into_response(); 213 + return ApiError::InvalidEmail.into_response(); 246 214 } 247 215 let verification_channel = input.verification_channel.as_deref().unwrap_or("email"); 248 216 let valid_channels = ["email", "discord", "telegram", "signal"]; 249 217 if !valid_channels.contains(&verification_channel) && !is_migration { 250 - return ( 251 - StatusCode::BAD_REQUEST, 252 - Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})), 253 - ) 254 - .into_response(); 218 + return ApiError::InvalidVerificationChannel.into_response(); 255 219 } 256 220 let verification_recipient = if is_migration { 257 221 None ··· 259 223 Some(match verification_channel { 260 224 "email" => match &input.email { 261 225 Some(email) if !email.trim().is_empty() => email.trim().to_string(), 262 - _ => return ( 263 - StatusCode::BAD_REQUEST, 264 - Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})), 265 - ).into_response(), 226 + _ => return ApiError::MissingEmail.into_response(), 266 227 }, 267 228 "discord" => match &input.discord_id { 268 229 Some(id) if !id.trim().is_empty() => id.trim().to_string(), 269 - _ => return ( 270 - StatusCode::BAD_REQUEST, 271 - Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})), 272 - ).into_response(), 230 + _ => return ApiError::MissingDiscordId.into_response(), 273 231 }, 274 232 "telegram" => match &input.telegram_username { 275 233 Some(username) if !username.trim().is_empty() => username.trim().to_string(), 276 - _ => return ( 277 - StatusCode::BAD_REQUEST, 278 - Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})), 279 - ).into_response(), 234 + _ => return ApiError::MissingTelegramUsername.into_response(), 280 235 }, 281 236 "signal" => match &input.signal_number { 282 237 Some(number) if !number.trim().is_empty() => number.trim().to_string(), 283 - _ => return ( 284 - StatusCode::BAD_REQUEST, 285 - Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})), 286 - ).into_response(), 238 + _ => return ApiError::MissingSignalNumber.into_response(), 287 239 }, 288 - _ => return ( 289 - StatusCode::BAD_REQUEST, 290 - Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})), 291 - ).into_response(), 240 + _ => return ApiError::InvalidVerificationChannel.into_response(), 292 241 }) 293 242 }; 294 243 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 319 268 match reserved { 320 269 Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 321 270 Ok(None) => { 322 - return ( 323 - StatusCode::BAD_REQUEST, 324 - Json(json!({ 325 - "error": "InvalidSigningKey", 326 - "message": "Signing key not found, already used, or expired" 327 - })), 328 - ) 329 - .into_response(); 271 + return ApiError::InvalidSigningKey.into_response(); 330 272 } 331 273 Err(e) => { 332 274 error!("Error looking up reserved signing key: {:?}", e); 333 - return ( 334 - StatusCode::INTERNAL_SERVER_ERROR, 335 - Json(json!({"error": "InternalError"})), 336 - ) 337 - .into_response(); 275 + return ApiError::InternalError(None).into_response(); 338 276 } 339 277 } 340 278 } else { ··· 345 283 Ok(k) => k, 346 284 Err(e) => { 347 285 error!("Error creating signing key: {:?}", e); 348 - return ( 349 - StatusCode::INTERNAL_SERVER_ERROR, 350 - Json(json!({"error": "InternalError"})), 351 - ) 352 - .into_response(); 286 + return ApiError::InternalError(None).into_response(); 353 287 } 354 288 }; 355 289 let did_type = input.did_type.as_deref().unwrap_or("plc"); 356 290 let did = match did_type { 357 291 "web" => { 358 292 if !crate::api::server::meta::is_self_hosted_did_web_enabled() { 359 - return ( 360 - StatusCode::BAD_REQUEST, 361 - Json(json!({ 362 - "error": "SelfHostedDidWebDisabled", 363 - "message": "This PDS does not offer self-hosted did:web identities. Please use did:plc or bring your own did:web." 364 - })), 365 - ) 366 - .into_response(); 293 + return ApiError::SelfHostedDidWebDisabled.into_response(); 367 294 } 368 295 let subdomain_host = format!("{}.{}", input.handle, hostname); 369 296 let encoded_subdomain = subdomain_host.replace(':', "%3A"); ··· 375 302 let d = match &input.did { 376 303 Some(d) if !d.trim().is_empty() => d, 377 304 _ => { 378 - return ( 379 - StatusCode::BAD_REQUEST, 380 - Json(json!({"error": "InvalidRequest", "message": "External did:web requires the 'did' field to be provided"})), 305 + return ApiError::InvalidRequest( 306 + "External did:web requires the 'did' field to be provided".into(), 381 307 ) 382 - .into_response(); 308 + .into_response(); 383 309 } 384 310 }; 385 311 if !d.starts_with("did:web:") { 386 - return ( 387 - StatusCode::BAD_REQUEST, 388 - Json( 389 - json!({"error": "InvalidDid", "message": "External DID must be a did:web"}), 390 - ), 391 - ) 312 + return ApiError::InvalidDid("External DID must be a did:web".into()) 392 313 .into_response(); 393 314 } 394 315 if !is_did_web_byod 395 316 && let Err(e) = 396 317 verify_did_web(d, &hostname, &input.handle, input.signing_key.as_deref()).await 397 318 { 398 - return ( 399 - StatusCode::BAD_REQUEST, 400 - Json(json!({"error": "InvalidDid", "message": e})), 401 - ) 402 - .into_response(); 319 + return ApiError::InvalidDid(e).into_response(); 403 320 } 404 321 info!(did = %d, "Creating external did:web account"); 405 322 d.clone() ··· 419 336 ) 420 337 .await 421 338 { 422 - return ( 423 - StatusCode::BAD_REQUEST, 424 - Json(json!({"error": "InvalidDid", "message": e})), 425 - ) 426 - .into_response(); 339 + return ApiError::InvalidDid(e).into_response(); 427 340 } 428 341 d.clone() 429 342 } else if !d.trim().is_empty() { 430 - return ( 431 - StatusCode::BAD_REQUEST, 432 - Json(json!({"error": "InvalidDid", "message": "Only did:web DIDs can be provided; leave empty for did:plc. For migration with existing did:plc, provide service auth."})), 343 + return ApiError::InvalidDid( 344 + "Only did:web DIDs can be provided; leave empty for did:plc. For migration with existing did:plc, provide service auth.".into() 433 345 ) 434 - .into_response(); 346 + .into_response(); 435 347 } else { 436 348 let rotation_key = std::env::var("PLC_ROTATION_KEY") 437 349 .unwrap_or_else(|_| signing_key_to_did_key(&signing_key)); ··· 444 356 Ok(r) => r, 445 357 Err(e) => { 446 358 error!("Error creating PLC genesis operation: {:?}", e); 447 - return ( 448 - StatusCode::INTERNAL_SERVER_ERROR, 449 - Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 450 - ) 451 - .into_response(); 359 + return ApiError::InternalError(Some( 360 + "Failed to create PLC operation".into(), 361 + )) 362 + .into_response(); 452 363 } 453 364 }; 454 365 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); ··· 457 368 .await 458 369 { 459 370 error!("Failed to submit PLC genesis operation: {:?}", e); 460 - return ( 461 - StatusCode::BAD_GATEWAY, 462 - Json(json!({ 463 - "error": "UpstreamError", 464 - "message": format!("Failed to register DID with PLC directory: {}", e) 465 - })), 466 - ) 467 - .into_response(); 371 + return ApiError::UpstreamErrorMsg(format!( 372 + "Failed to register DID with PLC directory: {}", 373 + e 374 + )) 375 + .into_response(); 468 376 } 469 377 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 470 378 genesis_result.did ··· 481 389 Ok(r) => r, 482 390 Err(e) => { 483 391 error!("Error creating PLC genesis operation: {:?}", e); 484 - return ( 485 - StatusCode::INTERNAL_SERVER_ERROR, 486 - Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 487 - ) 488 - .into_response(); 392 + return ApiError::InternalError(Some( 393 + "Failed to create PLC operation".into(), 394 + )) 395 + .into_response(); 489 396 } 490 397 }; 491 398 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); ··· 494 401 .await 495 402 { 496 403 error!("Failed to submit PLC genesis operation: {:?}", e); 497 - return ( 498 - StatusCode::BAD_GATEWAY, 499 - Json(json!({ 500 - "error": "UpstreamError", 501 - "message": format!("Failed to register DID with PLC directory: {}", e) 502 - })), 503 - ) 504 - .into_response(); 404 + return ApiError::UpstreamErrorMsg(format!( 405 + "Failed to register DID with PLC directory: {}", 406 + e 407 + )) 408 + .into_response(); 505 409 } 506 410 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 507 411 genesis_result.did ··· 512 416 Ok(tx) => tx, 513 417 Err(e) => { 514 418 error!("Error starting transaction: {:?}", e); 515 - return ( 516 - StatusCode::INTERNAL_SERVER_ERROR, 517 - Json(json!({"error": "InternalError"})), 518 - ) 519 - .into_response(); 419 + return ApiError::InternalError(None).into_response(); 520 420 } 521 421 }; 522 422 if is_migration { ··· 542 442 .map(|c| c.contains("handle")) 543 443 .unwrap_or(false) 544 444 { 545 - return ( 546 - StatusCode::BAD_REQUEST, 547 - Json(json!({"error": "HandleTaken", "message": "Handle already taken by another account"})), 548 - ) 549 - .into_response(); 445 + return ApiError::HandleTaken.into_response(); 550 446 } 551 447 error!("Error reactivating account: {:?}", e); 552 - return ( 553 - StatusCode::INTERNAL_SERVER_ERROR, 554 - Json(json!({"error": "InternalError"})), 555 - ) 556 - .into_response(); 448 + return ApiError::InternalError(None).into_response(); 557 449 } 558 450 if let Err(e) = tx.commit().await { 559 451 error!("Error committing reactivation: {:?}", e); 560 - return ( 561 - StatusCode::INTERNAL_SERVER_ERROR, 562 - Json(json!({"error": "InternalError"})), 563 - ) 564 - .into_response(); 452 + return ApiError::InternalError(None).into_response(); 565 453 } 566 454 let key_row: Option<(Vec<u8>, i32)> = sqlx::query_as( 567 455 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", ··· 576 464 Ok(k) => k, 577 465 Err(e) => { 578 466 error!("Error decrypting key for reactivated account: {:?}", e); 579 - return ( 580 - StatusCode::INTERNAL_SERVER_ERROR, 581 - Json(json!({"error": "InternalError"})), 582 - ) 583 - .into_response(); 467 + return ApiError::InternalError(None).into_response(); 584 468 } 585 469 } 586 470 } 587 471 None => { 588 472 error!("No signing key found for reactivated account"); 589 - return ( 590 - StatusCode::INTERNAL_SERVER_ERROR, 591 - Json(json!({"error": "InternalError", "message": "Account signing key not found"})), 592 - ) 593 - .into_response(); 473 + return ApiError::InternalError(Some( 474 + "Account signing key not found".into(), 475 + )) 476 + .into_response(); 594 477 } 595 478 }; 596 479 let access_meta = ··· 598 481 Ok(m) => m, 599 482 Err(e) => { 600 483 error!("Error creating access token: {:?}", e); 601 - return ( 602 - StatusCode::INTERNAL_SERVER_ERROR, 603 - Json(json!({"error": "InternalError"})), 604 - ) 605 - .into_response(); 484 + return ApiError::InternalError(None).into_response(); 606 485 } 607 486 }; 608 487 let refresh_meta = match crate::auth::create_refresh_token_with_metadata( ··· 612 491 Ok(m) => m, 613 492 Err(e) => { 614 493 error!("Error creating refresh token: {:?}", e); 615 - return ( 616 - StatusCode::INTERNAL_SERVER_ERROR, 617 - Json(json!({"error": "InternalError"})), 618 - ) 619 - .into_response(); 494 + return ApiError::InternalError(None).into_response(); 620 495 } 621 496 }; 622 497 let session_result: Result<_, sqlx::Error> = sqlx::query( ··· 631 506 .await; 632 507 if let Err(e) = session_result { 633 508 error!("Error creating session: {:?}", e); 634 - return ( 635 - StatusCode::INTERNAL_SERVER_ERROR, 636 - Json(json!({"error": "InternalError"})), 637 - ) 638 - .into_response(); 509 + return ApiError::InternalError(None).into_response(); 639 510 } 640 511 return ( 641 - StatusCode::OK, 512 + axum::http::StatusCode::OK, 642 513 Json(CreateAccountOutput { 643 - handle: handle.clone(), 644 - did: did.clone(), 514 + handle: handle.clone().into(), 515 + did: did.clone().into(), 645 516 did_doc: state.did_resolver.resolve_did_document(&did).await, 646 517 access_jwt: access_meta.token, 647 518 refresh_jwt: refresh_meta.token, ··· 651 522 ) 652 523 .into_response(); 653 524 } else { 654 - return ( 655 - StatusCode::BAD_REQUEST, 656 - Json(json!({"error": "AccountAlreadyExists", "message": "An active account with this DID already exists"})), 657 - ) 658 - .into_response(); 525 + return ApiError::AccountAlreadyExists.into_response(); 659 526 } 660 527 } 661 528 } ··· 666 533 .await 667 534 .unwrap_or(None); 668 535 if exists_result.is_some() { 669 - return ( 670 - StatusCode::BAD_REQUEST, 671 - Json(json!({"error": "HandleTaken", "message": "Handle already taken"})), 672 - ) 673 - .into_response(); 536 + return ApiError::HandleTaken.into_response(); 674 537 } 675 538 let invite_code_required = std::env::var("INVITE_CODE_REQUIRED") 676 539 .map(|v| v == "true" || v == "1") ··· 682 545 .map(|c| c.trim().is_empty()) 683 546 .unwrap_or(true) 684 547 { 685 - return ( 686 - StatusCode::BAD_REQUEST, 687 - Json(json!({"error": "InvalidInviteCode", "message": "Invite code is required"})), 688 - ) 689 - .into_response(); 548 + return ApiError::InviteCodeRequired.into_response(); 690 549 } 691 550 if let Some(code) = &input.invite_code 692 551 && !code.trim().is_empty() ··· 700 559 match invite_query { 701 560 Ok(Some(row)) => { 702 561 if row.available_uses <= 0 { 703 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response(); 562 + return ApiError::InvalidInviteCode.into_response(); 704 563 } 705 564 let update_invite = sqlx::query!( 706 565 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", ··· 710 569 .await; 711 570 if let Err(e) = update_invite { 712 571 error!("Error updating invite code: {:?}", e); 713 - return ( 714 - StatusCode::INTERNAL_SERVER_ERROR, 715 - Json(json!({"error": "InternalError"})), 716 - ) 717 - .into_response(); 572 + return ApiError::InternalError(None).into_response(); 718 573 } 719 574 } 720 575 Ok(None) => { 721 - return ( 722 - StatusCode::BAD_REQUEST, 723 - Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})), 724 - ) 725 - .into_response(); 576 + return ApiError::InvalidInviteCode.into_response(); 726 577 } 727 578 Err(e) => { 728 579 error!("Error checking invite code: {:?}", e); 729 - return ( 730 - StatusCode::INTERNAL_SERVER_ERROR, 731 - Json(json!({"error": "InternalError"})), 732 - ) 733 - .into_response(); 580 + return ApiError::InternalError(None).into_response(); 734 581 } 735 582 } 736 583 } 737 584 if let Err(e) = validate_password(&input.password) { 738 - return ( 739 - StatusCode::BAD_REQUEST, 740 - Json(json!({ 741 - "error": "InvalidPassword", 742 - "message": e.to_string() 743 - })), 744 - ) 745 - .into_response(); 585 + return ApiError::InvalidRequest(e.to_string()).into_response(); 746 586 } 747 587 748 588 let password_clone = input.password.clone(); ··· 751 591 Ok(Ok(h)) => h, 752 592 Ok(Err(e)) => { 753 593 error!("Error hashing password: {:?}", e); 754 - return ( 755 - StatusCode::INTERNAL_SERVER_ERROR, 756 - Json(json!({"error": "InternalError"})), 757 - ) 758 - .into_response(); 594 + return ApiError::InternalError(None).into_response(); 759 595 } 760 596 Err(e) => { 761 597 error!("Failed to spawn blocking task: {:?}", e); 762 - return ( 763 - StatusCode::INTERNAL_SERVER_ERROR, 764 - Json(json!({"error": "InternalError"})), 765 - ) 766 - .into_response(); 598 + return ApiError::InternalError(None).into_response(); 767 599 } 768 600 }; 769 601 let is_first_user = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users") ··· 823 655 { 824 656 let constraint = db_err.constraint().unwrap_or(""); 825 657 if constraint.contains("handle") || constraint.contains("users_handle") { 826 - return ( 827 - StatusCode::BAD_REQUEST, 828 - Json(json!({ 829 - "error": "HandleNotAvailable", 830 - "message": "Handle already taken" 831 - })), 832 - ) 833 - .into_response(); 658 + return ApiError::HandleNotAvailable(None).into_response(); 834 659 } else if constraint.contains("email") || constraint.contains("users_email") { 835 - return ( 836 - StatusCode::BAD_REQUEST, 837 - Json(json!({ 838 - "error": "InvalidEmail", 839 - "message": "Email already registered" 840 - })), 841 - ) 842 - .into_response(); 660 + return ApiError::EmailTaken.into_response(); 843 661 } else if constraint.contains("did") || constraint.contains("users_did") { 844 - return ( 845 - StatusCode::BAD_REQUEST, 846 - Json(json!({ 847 - "error": "AccountAlreadyExists", 848 - "message": "An account with this DID already exists" 849 - })), 850 - ) 851 - .into_response(); 662 + return ApiError::AccountAlreadyExists.into_response(); 852 663 } 853 664 } 854 665 error!("Error inserting user: {:?}", e); 855 - return ( 856 - StatusCode::INTERNAL_SERVER_ERROR, 857 - Json(json!({"error": "InternalError"})), 858 - ) 859 - .into_response(); 666 + return ApiError::InternalError(None).into_response(); 860 667 } 861 668 }; 862 669 ··· 864 671 Ok(enc) => enc, 865 672 Err(e) => { 866 673 error!("Error encrypting user key: {:?}", e); 867 - return ( 868 - StatusCode::INTERNAL_SERVER_ERROR, 869 - Json(json!({"error": "InternalError"})), 870 - ) 871 - .into_response(); 674 + return ApiError::InternalError(None).into_response(); 872 675 } 873 676 }; 874 677 let key_insert = sqlx::query!( ··· 881 684 .await; 882 685 if let Err(e) = key_insert { 883 686 error!("Error inserting user key: {:?}", e); 884 - return ( 885 - StatusCode::INTERNAL_SERVER_ERROR, 886 - Json(json!({"error": "InternalError"})), 887 - ) 888 - .into_response(); 687 + return ApiError::InternalError(None).into_response(); 889 688 } 890 689 if let Some(key_id) = reserved_key_id { 891 690 let mark_used = sqlx::query!( ··· 896 695 .await; 897 696 if let Err(e) = mark_used { 898 697 error!("Error marking reserved key as used: {:?}", e); 899 - return ( 900 - StatusCode::INTERNAL_SERVER_ERROR, 901 - Json(json!({"error": "InternalError"})), 902 - ) 903 - .into_response(); 698 + return ApiError::InternalError(None).into_response(); 904 699 } 905 700 } 906 701 let mst = Mst::new(Arc::new(state.block_store.clone())); ··· 908 703 Ok(c) => c, 909 704 Err(e) => { 910 705 error!("Error persisting MST: {:?}", e); 911 - return ( 912 - StatusCode::INTERNAL_SERVER_ERROR, 913 - Json(json!({"error": "InternalError"})), 914 - ) 915 - .into_response(); 706 + return ApiError::InternalError(None).into_response(); 916 707 } 917 708 }; 918 709 let rev = Tid::now(LimitedU32::MIN); ··· 921 712 Ok(result) => result, 922 713 Err(e) => { 923 714 error!("Error creating genesis commit: {:?}", e); 924 - return ( 925 - StatusCode::INTERNAL_SERVER_ERROR, 926 - Json(json!({"error": "InternalError"})), 927 - ) 928 - .into_response(); 715 + return ApiError::InternalError(None).into_response(); 929 716 } 930 717 }; 931 718 let commit_cid = match state.block_store.put(&commit_bytes).await { 932 719 Ok(c) => c, 933 720 Err(e) => { 934 721 error!("Error saving genesis commit: {:?}", e); 935 - return ( 936 - StatusCode::INTERNAL_SERVER_ERROR, 937 - Json(json!({"error": "InternalError"})), 938 - ) 939 - .into_response(); 722 + return ApiError::InternalError(None).into_response(); 940 723 } 941 724 }; 942 725 let commit_cid_str = commit_cid.to_string(); ··· 951 734 .await; 952 735 if let Err(e) = repo_insert { 953 736 error!("Error initializing repo: {:?}", e); 954 - return ( 955 - StatusCode::INTERNAL_SERVER_ERROR, 956 - Json(json!({"error": "InternalError"})), 957 - ) 958 - .into_response(); 737 + return ApiError::InternalError(None).into_response(); 959 738 } 960 739 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; 961 740 if let Err(e) = sqlx::query!( ··· 971 750 .await 972 751 { 973 752 error!("Error inserting user_blocks: {:?}", e); 974 - return ( 975 - StatusCode::INTERNAL_SERVER_ERROR, 976 - Json(json!({"error": "InternalError"})), 977 - ) 978 - .into_response(); 753 + return ApiError::InternalError(None).into_response(); 979 754 } 980 755 if let Some(code) = &input.invite_code 981 756 && !code.trim().is_empty() ··· 989 764 .await; 990 765 if let Err(e) = use_insert { 991 766 error!("Error recording invite usage: {:?}", e); 992 - return ( 993 - StatusCode::INTERNAL_SERVER_ERROR, 994 - Json(json!({"error": "InternalError"})), 995 - ) 996 - .into_response(); 767 + return ApiError::InternalError(None).into_response(); 997 768 } 998 769 } 999 770 if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() { ··· 1016 787 } 1017 788 if let Err(e) = tx.commit().await { 1018 789 error!("Error committing transaction: {:?}", e); 1019 - return ( 1020 - StatusCode::INTERNAL_SERVER_ERROR, 1021 - Json(json!({"error": "InternalError"})), 1022 - ) 1023 - .into_response(); 790 + return ApiError::InternalError(None).into_response(); 1024 791 } 1025 792 if !is_migration && !is_did_web_byod { 1026 793 if let Err(e) = ··· 1117 884 Ok(m) => m, 1118 885 Err(e) => { 1119 886 error!("createAccount: Error creating access token: {:?}", e); 1120 - return ( 1121 - StatusCode::INTERNAL_SERVER_ERROR, 1122 - Json(json!({"error": "InternalError"})), 1123 - ) 1124 - .into_response(); 887 + return ApiError::InternalError(None).into_response(); 1125 888 } 1126 889 }; 1127 890 let refresh_meta = ··· 1129 892 Ok(m) => m, 1130 893 Err(e) => { 1131 894 error!("createAccount: Error creating refresh token: {:?}", e); 1132 - return ( 1133 - StatusCode::INTERNAL_SERVER_ERROR, 1134 - Json(json!({"error": "InternalError"})), 1135 - ) 1136 - .into_response(); 895 + return ApiError::InternalError(None).into_response(); 1137 896 } 1138 897 }; 1139 898 if let Err(e) = sqlx::query!( ··· 1148 907 .await 1149 908 { 1150 909 error!("createAccount: Error creating session: {:?}", e); 1151 - return ( 1152 - StatusCode::INTERNAL_SERVER_ERROR, 1153 - Json(json!({"error": "InternalError"})), 1154 - ) 1155 - .into_response(); 910 + return ApiError::InternalError(None).into_response(); 1156 911 } 1157 912 1158 913 let did_doc = state.did_resolver.resolve_did_document(&did).await; ··· 1167 922 ( 1168 923 StatusCode::OK, 1169 924 Json(CreateAccountOutput { 1170 - handle: handle.clone(), 1171 - did, 925 + handle: handle.clone().into(), 926 + did: did.into(), 1172 927 did_doc, 1173 928 access_jwt: access_meta.token, 1174 929 refresh_jwt: refresh_meta.token,
+56 -175
src/api/identity/did.rs
··· 1 - use crate::api::ApiError; 1 + use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 2 use crate::plc::signing_key_to_did_key; 3 3 use crate::state::AppState; 4 4 use axum::{ ··· 34 34 ) -> Response { 35 35 let handle = params.handle.trim(); 36 36 if handle.is_empty() { 37 - return ( 38 - StatusCode::BAD_REQUEST, 39 - Json(json!({"error": "InvalidRequest", "message": "handle is required"})), 40 - ) 41 - .into_response(); 37 + return ApiError::InvalidRequest("handle is required".into()).into_response(); 42 38 } 43 39 let cache_key = format!("handle:{}", handle); 44 40 if let Some(did) = state.cache.get(&cache_key).await { 45 - return (StatusCode::OK, Json(json!({ "did": did }))).into_response(); 41 + return DidResponse::new(did).into_response(); 46 42 } 47 43 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle) 48 44 .fetch_optional(&state.db) ··· 53 49 .cache 54 50 .set(&cache_key, &row.did, std::time::Duration::from_secs(300)) 55 51 .await; 56 - (StatusCode::OK, Json(json!({ "did": row.did }))).into_response() 52 + DidResponse::new(row.did).into_response() 57 53 } 58 54 Ok(None) => match crate::handle::resolve_handle(handle).await { 59 55 Ok(did) => { ··· 61 57 .cache 62 58 .set(&cache_key, &did, std::time::Duration::from_secs(300)) 63 59 .await; 64 - (StatusCode::OK, Json(json!({ "did": did }))).into_response() 60 + DidResponse::new(did).into_response() 65 61 } 66 - Err(_) => ( 67 - StatusCode::NOT_FOUND, 68 - Json(json!({"error": "HandleNotFound", "message": "Unable to resolve handle"})), 69 - ) 70 - .into_response(), 62 + Err(_) => ApiError::HandleNotFound.into_response(), 71 63 }, 72 64 Err(e) => { 73 65 error!("DB error resolving handle: {:?}", e); 74 - ( 75 - StatusCode::INTERNAL_SERVER_ERROR, 76 - Json(json!({"error": "InternalError"})), 77 - ) 78 - .into_response() 66 + ApiError::InternalError(None).into_response() 79 67 } 80 68 } 81 69 } ··· 150 138 let (user_id, did, migrated_to_pds) = match user { 151 139 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds), 152 140 Ok(None) => { 153 - return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(); 141 + return ApiError::NotFoundMsg("User not found".into()).into_response(); 154 142 } 155 143 Err(e) => { 156 144 error!("DB Error: {:?}", e); 157 - return ( 158 - StatusCode::INTERNAL_SERVER_ERROR, 159 - Json(json!({"error": "InternalError"})), 160 - ) 161 - .into_response(); 145 + return ApiError::InternalError(None).into_response(); 162 146 } 163 147 }; 164 148 if !did.starts_with("did:web:") { 165 - return ( 166 - StatusCode::NOT_FOUND, 167 - Json(json!({"error": "NotFound", "message": "User is not did:web"})), 168 - ) 169 - .into_response(); 149 + return ApiError::NotFoundMsg("User is not did:web".into()).into_response(); 170 150 } 171 151 let subdomain_host = format!("{}.{}", handle, hostname); 172 152 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 173 153 let expected_self_hosted = format!("did:web:{}", encoded_subdomain); 174 154 if did != expected_self_hosted { 175 - return ( 176 - StatusCode::NOT_FOUND, 177 - Json(json!({"error": "NotFound", "message": "External did:web - DID document hosted by user"})), 178 - ) 155 + return ApiError::NotFoundMsg("External did:web - DID document hosted by user".into()) 179 156 .into_response(); 180 157 } 181 158 ··· 235 212 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 236 213 Ok(k) => k, 237 214 Err(_) => { 238 - return ( 239 - StatusCode::INTERNAL_SERVER_ERROR, 240 - Json(json!({"error": "InternalError"})), 241 - ) 242 - .into_response(); 215 + return ApiError::InternalError(None).into_response(); 243 216 } 244 217 }, 245 218 _ => { 246 - return ( 247 - StatusCode::INTERNAL_SERVER_ERROR, 248 - Json(json!({"error": "InternalError"})), 249 - ) 250 - .into_response(); 219 + return ApiError::InternalError(None).into_response(); 251 220 } 252 221 }; 253 222 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 254 223 Ok(pk) => pk, 255 224 Err(e) => { 256 225 tracing::error!("Failed to generate public key multibase: {}", e); 257 - return ( 258 - StatusCode::INTERNAL_SERVER_ERROR, 259 - Json(json!({"error": "InternalError"})), 260 - ) 261 - .into_response(); 226 + return ApiError::InternalError(None).into_response(); 262 227 } 263 228 }; 264 229 ··· 307 272 let (user_id, did, migrated_to_pds) = match user { 308 273 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds), 309 274 Ok(None) => { 310 - return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(); 275 + return ApiError::NotFoundMsg("User not found".into()).into_response(); 311 276 } 312 277 Err(e) => { 313 278 error!("DB Error: {:?}", e); 314 - return ( 315 - StatusCode::INTERNAL_SERVER_ERROR, 316 - Json(json!({"error": "InternalError"})), 317 - ) 318 - .into_response(); 279 + return ApiError::InternalError(None).into_response(); 319 280 } 320 281 }; 321 282 if !did.starts_with("did:web:") { 322 - return ( 323 - StatusCode::NOT_FOUND, 324 - Json(json!({"error": "NotFound", "message": "User is not did:web"})), 325 - ) 326 - .into_response(); 283 + return ApiError::NotFoundMsg("User is not did:web".into()).into_response(); 327 284 } 328 285 let encoded_hostname = hostname.replace(':', "%3A"); 329 286 let old_path_format = format!("did:web:{}:u:{}", encoded_hostname, handle); ··· 331 288 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 332 289 let new_subdomain_format = format!("did:web:{}", encoded_subdomain); 333 290 if did != old_path_format && did != new_subdomain_format { 334 - return ( 335 - StatusCode::NOT_FOUND, 336 - Json(json!({"error": "NotFound", "message": "External did:web - DID document hosted by user"})), 337 - ) 291 + return ApiError::NotFoundMsg("External did:web - DID document hosted by user".into()) 338 292 .into_response(); 339 293 } 340 294 ··· 394 348 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 395 349 Ok(k) => k, 396 350 Err(_) => { 397 - return ( 398 - StatusCode::INTERNAL_SERVER_ERROR, 399 - Json(json!({"error": "InternalError"})), 400 - ) 401 - .into_response(); 351 + return ApiError::InternalError(None).into_response(); 402 352 } 403 353 }, 404 354 _ => { 405 - return ( 406 - StatusCode::INTERNAL_SERVER_ERROR, 407 - Json(json!({"error": "InternalError"})), 408 - ) 409 - .into_response(); 355 + return ApiError::InternalError(None).into_response(); 410 356 } 411 357 }; 412 358 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 413 359 Ok(pk) => pk, 414 360 Err(e) => { 415 361 tracing::error!("Failed to generate public key multibase: {}", e); 416 - return ( 417 - StatusCode::INTERNAL_SERVER_ERROR, 418 - Json(json!({"error": "InternalError"})), 419 - ) 420 - .into_response(); 362 + return ApiError::InternalError(None).into_response(); 421 363 } 422 364 }; 423 365 ··· 587 529 ) { 588 530 Some(t) => t, 589 531 None => { 590 - return ( 591 - StatusCode::UNAUTHORIZED, 592 - Json(json!({"error": "AuthenticationRequired"})), 593 - ) 594 - .into_response(); 532 + return ApiError::AuthenticationRequired.into_response(); 595 533 } 596 534 }; 597 535 let auth_user = ··· 601 539 }; 602 540 let user = match sqlx::query!( 603 541 "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", 604 - auth_user.did 542 + &auth_user.did 605 543 ) 606 544 .fetch_optional(&state.db) 607 545 .await 608 546 { 609 547 Ok(Some(row)) => row, 610 - _ => return ApiError::InternalError.into_response(), 548 + _ => return ApiError::InternalError(None).into_response(), 611 549 }; 612 550 let key_bytes = match auth_user.key_bytes { 613 551 Some(kb) => kb, 614 552 None => { 615 - return ApiError::AuthenticationFailedMsg( 553 + return ApiError::AuthenticationFailed(Some( 616 554 "OAuth tokens cannot get DID credentials".into(), 617 - ) 555 + )) 618 556 .into_response(); 619 557 } 620 558 }; ··· 622 560 let pds_endpoint = format!("https://{}", hostname); 623 561 let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) { 624 562 Ok(k) => k, 625 - Err(_) => return ApiError::InternalError.into_response(), 563 + Err(_) => return ApiError::InternalError(None).into_response(), 626 564 }; 627 565 let did_key = signing_key_to_did_key(&signing_key); 628 566 let rotation_keys = if auth_user.did.starts_with("did:web:") { ··· 689 627 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 690 628 .await 691 629 { 692 - return ( 693 - StatusCode::TOO_MANY_REQUESTS, 694 - Json(json!({"error": "RateLimitExceeded", "message": "Too many handle updates. Try again later."})), 695 - ) 696 - .into_response(); 630 + return ApiError::RateLimitExceeded(Some("Too many handle updates. Try again later.".into(),)) 631 + .into_response(); 697 632 } 698 633 if !state 699 634 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 700 635 .await 701 636 { 702 - return ( 703 - StatusCode::TOO_MANY_REQUESTS, 704 - Json(json!({"error": "RateLimitExceeded", "message": "Daily handle update limit exceeded."})), 705 - ) 637 + return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into())) 706 638 .into_response(); 707 639 } 708 - let user_row = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did) 640 + let user_row = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did.as_str()) 709 641 .fetch_optional(&state.db) 710 642 .await 711 643 { 712 644 Ok(Some(row)) => row, 713 - _ => return ApiError::InternalError.into_response(), 645 + _ => return ApiError::InternalError(None).into_response(), 714 646 }; 715 647 let user_id = user_row.id; 716 648 let current_handle = user_row.handle; ··· 722 654 .chars() 723 655 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 724 656 { 725 - return ( 726 - StatusCode::BAD_REQUEST, 727 - Json( 728 - json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}), 729 - ), 730 - ) 657 + return ApiError::InvalidHandle(Some("Handle contains invalid characters".into())) 731 658 .into_response(); 732 659 } 733 660 for segment in new_handle.split('.') { 734 661 if segment.is_empty() { 735 - return ( 736 - StatusCode::BAD_REQUEST, 737 - Json(json!({"error": "InvalidHandle", "message": "Handle contains empty segment"})), 738 - ) 662 + return ApiError::InvalidHandle(Some("Handle contains empty segment".into())) 739 663 .into_response(); 740 664 } 741 665 if segment.starts_with('-') || segment.ends_with('-') { 742 - return ( 743 - StatusCode::BAD_REQUEST, 744 - Json(json!({"error": "InvalidHandle", "message": "Handle segment cannot start or end with hyphen"})), 745 - ) 746 - .into_response(); 666 + return ApiError::InvalidHandle(Some("Handle segment cannot start or end with hyphen".into(),)) 667 + .into_response(); 747 668 } 748 669 } 749 670 if crate::moderation::has_explicit_slur(&new_handle) { 750 - return ( 751 - StatusCode::BAD_REQUEST, 752 - Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})), 753 - ) 671 + return ApiError::InvalidHandle(Some("Inappropriate language in handle".into())) 754 672 .into_response(); 755 673 } 756 674 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 774 692 { 775 693 warn!("Failed to sequence identity event for handle update: {}", e); 776 694 } 777 - return (StatusCode::OK, Json(json!({}))).into_response(); 695 + return EmptyResponse::ok().into_response(); 778 696 } 779 697 if short_part.contains('.') { 780 - return ( 781 - StatusCode::BAD_REQUEST, 782 - Json(json!({ 783 - "error": "InvalidHandle", 784 - "message": "Nested subdomains are not allowed. Use a simple handle without dots." 785 - })), 786 - ) 787 - .into_response(); 698 + return ApiError::InvalidHandle(Some("Nested subdomains are not allowed. Use a simple handle without dots.".into(),)) 699 + .into_response(); 788 700 } 789 701 if short_part.len() < 3 { 790 - return ( 791 - StatusCode::BAD_REQUEST, 792 - Json(json!({"error": "InvalidHandle", "message": "Handle too short"})), 793 - ) 794 - .into_response(); 702 + return ApiError::InvalidHandle(Some("Handle too short".into())).into_response(); 795 703 } 796 704 if short_part.len() > 18 { 797 - return ( 798 - StatusCode::BAD_REQUEST, 799 - Json(json!({"error": "InvalidHandle", "message": "Handle too long"})), 800 - ) 801 - .into_response(); 705 + return ApiError::InvalidHandle(Some("Handle too long".into())).into_response(); 802 706 } 803 707 full_handle 804 708 } else { ··· 809 713 { 810 714 warn!("Failed to sequence identity event for handle update: {}", e); 811 715 } 812 - return (StatusCode::OK, Json(json!({}))).into_response(); 716 + return EmptyResponse::ok().into_response(); 813 717 } 814 718 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 815 719 Ok(()) => {} 816 720 Err(crate::handle::HandleResolutionError::NotFound) => { 817 - return ( 818 - StatusCode::BAD_REQUEST, 819 - Json(json!({ 820 - "error": "HandleNotAvailable", 821 - "message": "Handle verification failed. Please set up DNS TXT record at _atproto.{} or serve your DID at https://{}/.well-known/atproto-did", 822 - "handle": new_handle 823 - })), 824 - ) 825 - .into_response(); 721 + return ApiError::HandleNotAvailable(None).into_response(); 826 722 } 827 723 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 828 - return ( 829 - StatusCode::BAD_REQUEST, 830 - Json(json!({ 831 - "error": "HandleNotAvailable", 832 - "message": format!("Handle points to different DID. Expected {}, got {}", expected, actual) 833 - })), 834 - ) 835 - .into_response(); 724 + return ApiError::HandleNotAvailable(Some( 725 + format!("Handle points to different DID. Expected {}, got {}", expected, actual), 726 + )) 727 + .into_response(); 836 728 } 837 729 Err(e) => { 838 730 warn!("Handle verification failed: {}", e); 839 - return ( 840 - StatusCode::BAD_REQUEST, 841 - Json(json!({ 842 - "error": "HandleNotAvailable", 843 - "message": format!("Handle verification failed: {}", e) 844 - })), 845 - ) 846 - .into_response(); 731 + return ApiError::HandleNotAvailable(Some(format!( 732 + "Handle verification failed: {}", 733 + e 734 + ))) 735 + .into_response(); 847 736 } 848 737 } 849 738 new_handle.clone() ··· 856 745 .fetch_optional(&state.db) 857 746 .await; 858 747 if let Ok(Some(_)) = existing { 859 - return ( 860 - StatusCode::BAD_REQUEST, 861 - Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})), 862 - ) 863 - .into_response(); 748 + return ApiError::HandleTaken.into_response(); 864 749 } 865 750 let result = sqlx::query!( 866 751 "UPDATE users SET handle = $1 WHERE id = $2", ··· 886 771 if let Err(e) = update_plc_handle(&state, &did, &handle).await { 887 772 warn!("Failed to update PLC handle: {}", e); 888 773 } 889 - (StatusCode::OK, Json(json!({}))).into_response() 774 + EmptyResponse::ok().into_response() 890 775 } 891 776 Err(e) => { 892 777 error!("DB error updating handle: {:?}", e); 893 - ( 894 - StatusCode::INTERNAL_SERVER_ERROR, 895 - Json(json!({"error": "InternalError"})), 896 - ) 897 - .into_response() 778 + ApiError::InternalError(None).into_response() 898 779 } 899 780 } 900 781 }
+6 -12
src/api/identity/plc/request.rs
··· 1 - use crate::api::ApiError; 1 + use crate::api::EmptyResponse; 2 + use crate::api::error::ApiError; 2 3 use crate::state::AppState; 3 4 use axum::{ 4 - Json, 5 5 extract::State, 6 - http::StatusCode, 7 6 response::{IntoResponse, Response}, 8 7 }; 9 8 use chrono::{Duration, Utc}; 10 - use serde_json::json; 11 9 use tracing::{error, info, warn}; 12 10 13 11 fn generate_plc_token() -> String { ··· 36 34 ) { 37 35 return e; 38 36 } 39 - let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", auth_user.did) 37 + let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", &auth_user.did) 40 38 .fetch_optional(&state.db) 41 39 .await 42 40 { ··· 44 42 Ok(None) => return ApiError::AccountNotFound.into_response(), 45 43 Err(e) => { 46 44 error!("DB error: {:?}", e); 47 - return ApiError::InternalError.into_response(); 45 + return ApiError::InternalError(None).into_response(); 48 46 } 49 47 }; 50 48 let _ = sqlx::query!( ··· 68 66 .await 69 67 { 70 68 error!("Failed to create PLC token: {:?}", e); 71 - return ( 72 - StatusCode::INTERNAL_SERVER_ERROR, 73 - Json(json!({"error": "InternalError"})), 74 - ) 75 - .into_response(); 69 + return ApiError::InternalError(None).into_response(); 76 70 } 77 71 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 78 72 if let Err(e) = ··· 84 78 "PLC operation signature requested for user {}", 85 79 auth_user.did 86 80 ); 87 - (StatusCode::OK, Json(json!({}))).into_response() 81 + EmptyResponse::ok().into_response() 88 82 }
+21 -111
src/api/identity/plc/sign.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::circuit_breaker::{CircuitBreakerError, with_circuit_breaker}; 3 - use crate::plc::{ 4 - PlcClient, PlcError, PlcOpOrTombstone, PlcService, create_update_op, sign_operation, 5 - }; 2 + use crate::circuit_breaker::with_circuit_breaker; 3 + use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 6 4 use crate::state::AppState; 7 5 use axum::{ 8 6 Json, ··· 13 11 use chrono::Utc; 14 12 use k256::ecdsa::SigningKey; 15 13 use serde::{Deserialize, Serialize}; 16 - use serde_json::{Value, json}; 14 + use serde_json::Value; 17 15 use std::collections::HashMap; 18 - use tracing::{error, info, warn}; 16 + use tracing::{error, info}; 19 17 20 18 #[derive(Debug, Deserialize)] 21 19 #[serde(rename_all = "camelCase")] ··· 84 82 { 85 83 Ok(Some(row)) => row, 86 84 _ => { 87 - return ( 88 - StatusCode::NOT_FOUND, 89 - Json(json!({"error": "AccountNotFound"})), 90 - ) 91 - .into_response(); 85 + return ApiError::AccountNotFound.into_response(); 92 86 } 93 87 }; 94 88 let token_row = match sqlx::query!( ··· 101 95 { 102 96 Ok(Some(row)) => row, 103 97 Ok(None) => { 104 - return ( 105 - StatusCode::BAD_REQUEST, 106 - Json(json!({ 107 - "error": "InvalidToken", 108 - "message": "Invalid or expired token" 109 - })), 110 - ) 111 - .into_response(); 98 + return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response(); 112 99 } 113 100 Err(e) => { 114 101 error!("DB error: {:?}", e); 115 - return ( 116 - StatusCode::INTERNAL_SERVER_ERROR, 117 - Json(json!({"error": "InternalError"})), 118 - ) 119 - .into_response(); 102 + return ApiError::InternalError(None).into_response(); 120 103 } 121 104 }; 122 105 if Utc::now() > token_row.expires_at { ··· 126 109 ) 127 110 .execute(&state.db) 128 111 .await; 129 - return ( 130 - StatusCode::BAD_REQUEST, 131 - Json(json!({ 132 - "error": "ExpiredToken", 133 - "message": "Token has expired" 134 - })), 135 - ) 136 - .into_response(); 112 + return ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); 137 113 } 138 114 let key_row = match sqlx::query!( 139 115 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", ··· 144 120 { 145 121 Ok(Some(row)) => row, 146 122 _ => { 147 - return ( 148 - StatusCode::INTERNAL_SERVER_ERROR, 149 - Json(json!({"error": "InternalError", "message": "User signing key not found"})), 150 - ) 151 - .into_response(); 123 + return ApiError::InternalError(Some("User signing key not found".into())).into_response(); 152 124 } 153 125 }; 154 126 let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) ··· 156 128 Ok(k) => k, 157 129 Err(e) => { 158 130 error!("Failed to decrypt user key: {}", e); 159 - return ( 160 - StatusCode::INTERNAL_SERVER_ERROR, 161 - Json(json!({"error": "InternalError"})), 162 - ) 163 - .into_response(); 131 + return ApiError::InternalError(None).into_response(); 164 132 } 165 133 }; 166 134 let signing_key = match SigningKey::from_slice(&key_bytes) { 167 135 Ok(k) => k, 168 136 Err(e) => { 169 137 error!("Failed to create signing key: {:?}", e); 170 - return ( 171 - StatusCode::INTERNAL_SERVER_ERROR, 172 - Json(json!({"error": "InternalError"})), 173 - ) 174 - .into_response(); 138 + return ApiError::InternalError(None).into_response(); 175 139 } 176 140 }; 177 141 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 178 142 let did_clone = did.clone(); 179 - let result: Result<PlcOpOrTombstone, CircuitBreakerError<PlcError>> = 180 - with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 181 - plc_client.get_last_op(&did_clone).await 182 - }) 183 - .await; 184 - let last_op = match result { 143 + let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 144 + plc_client.get_last_op(&did_clone).await 145 + }) 146 + .await 147 + { 185 148 Ok(op) => op, 186 - Err(CircuitBreakerError::CircuitOpen(e)) => { 187 - warn!("PLC directory circuit breaker open: {}", e); 188 - return ( 189 - StatusCode::SERVICE_UNAVAILABLE, 190 - Json(json!({ 191 - "error": "ServiceUnavailable", 192 - "message": "PLC directory service temporarily unavailable" 193 - })), 194 - ) 195 - .into_response(); 196 - } 197 - Err(CircuitBreakerError::OperationFailed(PlcError::NotFound)) => { 198 - return ( 199 - StatusCode::NOT_FOUND, 200 - Json(json!({ 201 - "error": "NotFound", 202 - "message": "DID not found in PLC directory" 203 - })), 204 - ) 205 - .into_response(); 206 - } 207 - Err(CircuitBreakerError::OperationFailed(e)) => { 208 - error!("Failed to fetch PLC operation: {:?}", e); 209 - return ( 210 - StatusCode::BAD_GATEWAY, 211 - Json(json!({ 212 - "error": "UpstreamError", 213 - "message": "Failed to communicate with PLC directory" 214 - })), 215 - ) 216 - .into_response(); 217 - } 149 + Err(e) => return ApiError::from(e).into_response(), 218 150 }; 219 151 if last_op.is_tombstone() { 220 - return ( 221 - StatusCode::BAD_REQUEST, 222 - Json(json!({ 223 - "error": "InvalidRequest", 224 - "message": "DID is tombstoned" 225 - })), 226 - ) 227 - .into_response(); 152 + return ApiError::from(PlcError::Tombstoned).into_response(); 228 153 } 229 154 let services = input.services.map(|s| { 230 155 s.into_iter() ··· 248 173 ) { 249 174 Ok(op) => op, 250 175 Err(PlcError::Tombstoned) => { 251 - return ( 252 - StatusCode::BAD_REQUEST, 253 - Json(json!({ 254 - "error": "InvalidRequest", 255 - "message": "Cannot update tombstoned DID" 256 - })), 257 - ) 258 - .into_response(); 176 + return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response(); 259 177 } 260 178 Err(e) => { 261 179 error!("Failed to create PLC operation: {:?}", e); 262 - return ( 263 - StatusCode::INTERNAL_SERVER_ERROR, 264 - Json(json!({"error": "InternalError"})), 265 - ) 266 - .into_response(); 180 + return ApiError::InternalError(None).into_response(); 267 181 } 268 182 }; 269 183 let signed_op = match sign_operation(&unsigned_op, &signing_key) { 270 184 Ok(op) => op, 271 185 Err(e) => { 272 186 error!("Failed to sign PLC operation: {:?}", e); 273 - return ( 274 - StatusCode::INTERNAL_SERVER_ERROR, 275 - Json(json!({"error": "InternalError"})), 276 - ) 277 - .into_response(); 187 + return ApiError::InternalError(None).into_response(); 278 188 } 279 189 }; 280 190 let _ = sqlx::query!(
+24 -92
src/api/identity/plc/submit.rs
··· 1 - use crate::api::ApiError; 2 - use crate::circuit_breaker::{CircuitBreakerError, with_circuit_breaker}; 3 - use crate::plc::{PlcClient, PlcError, signing_key_to_did_key, validate_plc_operation}; 1 + use crate::api::{ApiError, EmptyResponse}; 2 + use crate::circuit_breaker::with_circuit_breaker; 3 + use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 4 4 use crate::state::AppState; 5 5 use axum::{ 6 6 Json, 7 7 extract::State, 8 - http::StatusCode, 9 8 response::{IntoResponse, Response}, 10 9 }; 11 10 use k256::ecdsa::SigningKey; 12 11 use serde::Deserialize; 13 - use serde_json::{Value, json}; 12 + use serde_json::Value; 14 13 use tracing::{error, info, warn}; 15 14 16 15 #[derive(Debug, Deserialize)] ··· 64 63 { 65 64 Ok(Some(row)) => row, 66 65 _ => { 67 - return ( 68 - StatusCode::NOT_FOUND, 69 - Json(json!({"error": "AccountNotFound"})), 70 - ) 71 - .into_response(); 66 + return ApiError::AccountNotFound.into_response(); 72 67 } 73 68 }; 74 69 let key_row = match sqlx::query!( ··· 80 75 { 81 76 Ok(Some(row)) => row, 82 77 _ => { 83 - return ( 84 - StatusCode::INTERNAL_SERVER_ERROR, 85 - Json(json!({"error": "InternalError", "message": "User signing key not found"})), 86 - ) 87 - .into_response(); 78 + return ApiError::InternalError(Some("User signing key not found".into())).into_response(); 88 79 } 89 80 }; 90 81 let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) ··· 92 83 Ok(k) => k, 93 84 Err(e) => { 94 85 error!("Failed to decrypt user key: {}", e); 95 - return ( 96 - StatusCode::INTERNAL_SERVER_ERROR, 97 - Json(json!({"error": "InternalError"})), 98 - ) 99 - .into_response(); 86 + return ApiError::InternalError(None).into_response(); 100 87 } 101 88 }; 102 89 let signing_key = match SigningKey::from_slice(&key_bytes) { 103 90 Ok(k) => k, 104 91 Err(e) => { 105 92 error!("Failed to create signing key: {:?}", e); 106 - return ( 107 - StatusCode::INTERNAL_SERVER_ERROR, 108 - Json(json!({"error": "InternalError"})), 109 - ) 110 - .into_response(); 93 + return ApiError::InternalError(None).into_response(); 111 94 } 112 95 }; 113 96 let user_did_key = signing_key_to_did_key(&signing_key); ··· 118 101 .iter() 119 102 .any(|k| k.as_str() == Some(&server_rotation_key)); 120 103 if !has_server_key { 121 - return ( 122 - StatusCode::BAD_REQUEST, 123 - Json(json!({ 124 - "error": "InvalidRequest", 125 - "message": "Rotation keys do not include server's rotation key" 126 - })), 104 + return ApiError::InvalidRequest( 105 + "Rotation keys do not include server's rotation key".into(), 127 106 ) 128 - .into_response(); 107 + .into_response(); 129 108 } 130 109 } 131 110 if let Some(services) = op.get("services").and_then(|v| v.as_object()) ··· 134 113 let service_type = pds.get("type").and_then(|v| v.as_str()); 135 114 let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); 136 115 if service_type != Some("AtprotoPersonalDataServer") { 137 - return ( 138 - StatusCode::BAD_REQUEST, 139 - Json(json!({ 140 - "error": "InvalidRequest", 141 - "message": "Incorrect type on atproto_pds service" 142 - })), 143 - ) 116 + return ApiError::InvalidRequest("Incorrect type on atproto_pds service".into()) 144 117 .into_response(); 145 118 } 146 119 if endpoint != Some(&public_url) { 147 - return ( 148 - StatusCode::BAD_REQUEST, 149 - Json(json!({ 150 - "error": "InvalidRequest", 151 - "message": "Incorrect endpoint on atproto_pds service" 152 - })), 153 - ) 120 + return ApiError::InvalidRequest("Incorrect endpoint on atproto_pds service".into()) 154 121 .into_response(); 155 122 } 156 123 } ··· 158 125 && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 159 126 && atproto_key != user_did_key 160 127 { 161 - return ( 162 - StatusCode::BAD_REQUEST, 163 - Json(json!({ 164 - "error": "InvalidRequest", 165 - "message": "Incorrect signing key in verificationMethods" 166 - })), 167 - ) 128 + return ApiError::InvalidRequest("Incorrect signing key in verificationMethods".into()) 168 129 .into_response(); 169 130 } 170 131 if let Some(also_known_as) = (!user.handle.is_empty()) ··· 174 135 let expected_handle = format!("at://{}", user.handle); 175 136 let first_aka = also_known_as.first().and_then(|v| v.as_str()); 176 137 if first_aka != Some(&expected_handle) { 177 - return ( 178 - StatusCode::BAD_REQUEST, 179 - Json(json!({ 180 - "error": "InvalidRequest", 181 - "message": "Incorrect handle in alsoKnownAs" 182 - })), 183 - ) 138 + return ApiError::InvalidRequest("Incorrect handle in alsoKnownAs".into()) 184 139 .into_response(); 185 140 } 186 141 } 187 142 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 188 143 let operation_clone = input.operation.clone(); 189 144 let did_clone = did.clone(); 190 - let result: Result<(), CircuitBreakerError<PlcError>> = 191 - with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 192 - plc_client 193 - .send_operation(&did_clone, &operation_clone) 194 - .await 195 - }) 196 - .await; 197 - match result { 198 - Ok(()) => {} 199 - Err(CircuitBreakerError::CircuitOpen(e)) => { 200 - warn!("PLC directory circuit breaker open: {}", e); 201 - return ( 202 - StatusCode::SERVICE_UNAVAILABLE, 203 - Json(json!({ 204 - "error": "ServiceUnavailable", 205 - "message": "PLC directory service temporarily unavailable" 206 - })), 207 - ) 208 - .into_response(); 209 - } 210 - Err(CircuitBreakerError::OperationFailed(e)) => { 211 - error!("PLC operation failed: {:?}", e); 212 - return ( 213 - StatusCode::BAD_GATEWAY, 214 - Json(json!({ 215 - "error": "UpstreamError", 216 - "message": format!("Failed to submit to PLC directory: {}", e) 217 - })), 218 - ) 219 - .into_response(); 220 - } 145 + if let Err(e) = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 146 + plc_client 147 + .send_operation(&did_clone, &operation_clone) 148 + .await 149 + }) 150 + .await 151 + { 152 + return ApiError::from(e).into_response(); 221 153 } 222 154 match sqlx::query!( 223 155 "INSERT INTO repo_seq (did, event_type, handle) VALUES ($1, 'identity', $2) RETURNING seq", ··· 244 176 warn!(did = %did, "Failed to refresh DID cache after PLC update"); 245 177 } 246 178 info!(did = %did, "PLC operation submitted successfully"); 247 - (StatusCode::OK, Json(json!({}))).into_response() 179 + EmptyResponse::ok().into_response() 248 180 }
+5
src/api/mod.rs
··· 10 10 pub mod proxy; 11 11 pub mod proxy_client; 12 12 pub mod repo; 13 + pub mod responses; 13 14 pub mod server; 14 15 pub mod temp; 15 16 pub mod validation; 16 17 pub mod verification; 17 18 18 19 pub use error::ApiError; 20 + pub use responses::{ 21 + DidResponse, EmptyResponse, EnabledResponse, HasPasswordResponse, OptionsResponse, 22 + StatusResponse, SuccessResponse, TokenRequiredResponse, VerifiedResponse, 23 + }; 19 24 pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_did, validate_limit};
+10 -28
src/api/moderation/mod.rs
··· 64 64 .await; 65 65 } 66 66 67 - create_report_locally(&state, did, auth_user.is_takendown, input).await 67 + create_report_locally(&state, did, auth_user.is_takendown(), input).await 68 68 } 69 69 70 70 async fn proxy_to_report_service( ··· 76 76 ) -> Response { 77 77 if let Err(e) = is_ssrf_safe(service_url) { 78 78 error!("Report service URL failed SSRF check: {:?}", e); 79 - return ( 80 - StatusCode::INTERNAL_SERVER_ERROR, 81 - Json(json!({"error": "InternalError", "message": "Invalid report service configuration"})), 82 - ) 79 + return ApiError::InternalError(Some("Invalid report service configuration".into())) 83 80 .into_response(); 84 81 } 85 82 ··· 101 98 Ok(key) => key, 102 99 Err(e) => { 103 100 error!(error = ?e, "Failed to decrypt user key for report service auth"); 104 - return ApiError::AuthenticationFailedMsg( 101 + return ApiError::AuthenticationFailed(Some( 105 102 "Failed to get signing key".into(), 106 - ) 103 + )) 107 104 .into_response(); 108 105 } 109 106 } 110 107 } 111 108 Ok(None) => { 112 - return ApiError::AuthenticationFailedMsg("User has no signing key".into()) 109 + return ApiError::AuthenticationFailed(Some("User has no signing key".into())) 113 110 .into_response(); 114 111 } 115 112 Err(e) => { 116 113 error!(error = ?e, "DB error fetching user key for report"); 117 - return ApiError::AuthenticationFailedMsg("Failed to get signing key".into()) 114 + return ApiError::AuthenticationFailed(Some("Failed to get signing key".into())) 118 115 .into_response(); 119 116 } 120 117 } ··· 130 127 Ok(t) => t, 131 128 Err(e) => { 132 129 error!("Failed to create service token for report: {:?}", e); 133 - return ( 134 - StatusCode::INTERNAL_SERVER_ERROR, 135 - Json(json!({"error": "InternalError"})), 136 - ) 137 - .into_response(); 130 + return ApiError::InternalError(None).into_response(); 138 131 } 139 132 }; 140 133 ··· 208 201 const REASON_APPEAL: &str = "com.atproto.moderation.defs#reasonAppeal"; 209 202 210 203 if is_takendown && input.reason_type != REASON_APPEAL { 211 - return ( 212 - StatusCode::BAD_REQUEST, 213 - Json(json!({"error": "InvalidRequest", "message": "Report not accepted from takendown account"})), 214 - ) 204 + return ApiError::InvalidRequest("Report not accepted from takendown account".into()) 215 205 .into_response(); 216 206 } 217 207 ··· 226 216 ]; 227 217 228 218 if !valid_reason_types.contains(&input.reason_type.as_str()) { 229 - return ( 230 - StatusCode::BAD_REQUEST, 231 - Json(json!({"error": "InvalidRequest", "message": "Invalid reasonType"})), 232 - ) 233 - .into_response(); 219 + return ApiError::InvalidRequest("Invalid reasonType".into()).into_response(); 234 220 } 235 221 236 222 let created_at = chrono::Utc::now(); ··· 251 237 252 238 if let Err(e) = insert { 253 239 error!("Failed to insert report: {:?}", e); 254 - return ( 255 - StatusCode::INTERNAL_SERVER_ERROR, 256 - Json(json!({"error": "InternalError"})), 257 - ) 258 - .into_response(); 240 + return ApiError::InternalError(None).into_response(); 259 241 } 260 242 261 243 info!(
+36 -123
src/api/notification_prefs.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::auth::validate_bearer_token; 2 3 use crate::state::AppState; 3 4 use axum::{ 4 5 Json, 5 6 extract::State, 6 - http::{HeaderMap, StatusCode}, 7 + http::HeaderMap, 7 8 response::{IntoResponse, Response}, 8 9 }; 9 10 use serde::{Deserialize, Serialize}; ··· 29 30 headers.get("Authorization").and_then(|h| h.to_str().ok()), 30 31 ) { 31 32 Some(t) => t, 32 - None => return ( 33 - StatusCode::UNAUTHORIZED, 34 - Json(json!({"error": "AuthenticationRequired", "message": "Authentication required"})), 35 - ) 36 - .into_response(), 33 + None => return ApiError::AuthenticationRequired.into_response(), 37 34 }; 38 35 let user = match validate_bearer_token(&state.db, &token).await { 39 36 Ok(u) => u, 40 37 Err(_) => { 41 - return ( 42 - StatusCode::UNAUTHORIZED, 43 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token"})), 44 - ) 45 - .into_response(); 38 + return ApiError::AuthenticationFailed(None).into_response(); 46 39 } 47 40 }; 48 41 let row = ··· 66 59 .await 67 60 { 68 61 Ok(r) => r, 69 - Err(e) => return ( 70 - StatusCode::INTERNAL_SERVER_ERROR, 71 - Json( 72 - json!({"error": "InternalError", "message": format!("Database error: {}", e)}), 73 - ), 74 - ) 75 - .into_response(), 62 + Err(e) => { 63 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response() 64 + } 76 65 }; 77 66 let email: String = row.get("email"); 78 67 let channel: String = row.get("channel"); ··· 120 109 headers.get("Authorization").and_then(|h| h.to_str().ok()), 121 110 ) { 122 111 Some(t) => t, 123 - None => return ( 124 - StatusCode::UNAUTHORIZED, 125 - Json(json!({"error": "AuthenticationRequired", "message": "Authentication required"})), 126 - ) 127 - .into_response(), 112 + None => return ApiError::AuthenticationRequired.into_response(), 128 113 }; 129 114 let user = match validate_bearer_token(&state.db, &token).await { 130 115 Ok(u) => u, 131 116 Err(_) => { 132 - return ( 133 - StatusCode::UNAUTHORIZED, 134 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token"})), 135 - ) 136 - .into_response(); 117 + return ApiError::AuthenticationFailed(None).into_response(); 137 118 } 138 119 }; 139 120 140 121 let user_id: uuid::Uuid = 141 - match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", user.did) 122 + match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", &user.did) 142 123 .fetch_one(&state.db) 143 124 .await 144 125 { 145 126 Ok(id) => id, 146 - Err(e) => return ( 147 - StatusCode::INTERNAL_SERVER_ERROR, 148 - Json( 149 - json!({"error": "InternalError", "message": format!("Database error: {}", e)}), 150 - ), 151 - ) 152 - .into_response(), 127 + Err(e) => { 128 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response() 129 + } 153 130 }; 154 131 155 132 let rows = ··· 173 150 .await 174 151 { 175 152 Ok(r) => r, 176 - Err(e) => return ( 177 - StatusCode::INTERNAL_SERVER_ERROR, 178 - Json( 179 - json!({"error": "InternalError", "message": format!("Database error: {}", e)}), 180 - ), 181 - ) 182 - .into_response(), 153 + Err(e) => { 154 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response() 155 + } 183 156 }; 184 157 185 158 let sensitive_types = [ ··· 288 261 headers.get("Authorization").and_then(|h| h.to_str().ok()), 289 262 ) { 290 263 Some(t) => t, 291 - None => return ( 292 - StatusCode::UNAUTHORIZED, 293 - Json(json!({"error": "AuthenticationRequired", "message": "Authentication required"})), 294 - ) 295 - .into_response(), 264 + None => return ApiError::AuthenticationRequired.into_response(), 296 265 }; 297 266 let user = match validate_bearer_token(&state.db, &token).await { 298 267 Ok(u) => u, 299 268 Err(_) => { 300 - return ( 301 - StatusCode::UNAUTHORIZED, 302 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token"})), 303 - ) 304 - .into_response(); 269 + return ApiError::AuthenticationFailed(None).into_response(); 305 270 } 306 271 }; 307 272 308 273 let user_row = 309 274 match sqlx::query!( 310 275 "SELECT id, handle, email FROM users WHERE did = $1", 311 - user.did 276 + &user.did 312 277 ) 313 278 .fetch_one(&state.db) 314 279 .await 315 280 { 316 281 Ok(row) => row, 317 - Err(e) => return ( 318 - StatusCode::INTERNAL_SERVER_ERROR, 319 - Json( 320 - json!({"error": "InternalError", "message": format!("Database error: {}", e)}), 321 - ), 322 - ) 323 - .into_response(), 282 + Err(e) => { 283 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response() 284 + } 324 285 }; 325 286 326 287 let user_id = user_row.id; ··· 332 293 if let Some(ref channel) = input.preferred_channel { 333 294 let valid_channels = ["email", "discord", "telegram", "signal"]; 334 295 if !valid_channels.contains(&channel.as_str()) { 335 - return ( 336 - StatusCode::BAD_REQUEST, 337 - Json(json!({ 338 - "error": "InvalidRequest", 339 - "message": "Invalid channel. Must be one of: email, discord, telegram, signal" 340 - })), 296 + return ApiError::InvalidRequest( 297 + "Invalid channel. Must be one of: email, discord, telegram, signal".into(), 341 298 ) 342 - .into_response(); 299 + .into_response(); 343 300 } 344 301 if let Err(e) = sqlx::query( 345 302 r#"UPDATE users SET preferred_comms_channel = $1::comms_channel, updated_at = NOW() WHERE did = $2"# ··· 349 306 .execute(&state.db) 350 307 .await 351 308 { 352 - return ( 353 - StatusCode::INTERNAL_SERVER_ERROR, 354 - Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 355 - ) 356 - .into_response(); 309 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 357 310 } 358 311 info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); 359 312 } ··· 361 314 if let Some(ref new_email) = input.email { 362 315 let email_clean = new_email.trim().to_lowercase(); 363 316 if email_clean.is_empty() { 364 - return ( 365 - StatusCode::BAD_REQUEST, 366 - Json(json!({"error": "InvalidRequest", "message": "Email cannot be empty"})), 367 - ) 368 - .into_response(); 317 + return ApiError::InvalidRequest("Email cannot be empty".into()).into_response(); 369 318 } 370 319 371 320 if !crate::api::validation::is_valid_email(&email_clean) { 372 - return ( 373 - StatusCode::BAD_REQUEST, 374 - Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 375 - ) 376 - .into_response(); 321 + return ApiError::InvalidEmail.into_response(); 377 322 } 378 323 379 324 if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email_clean.clone()) { ··· 388 333 .await; 389 334 390 335 if let Ok(Some(_)) = exists { 391 - return ( 392 - StatusCode::BAD_REQUEST, 393 - Json(json!({"error": "EmailTaken", "message": "Email already in use"})), 394 - ) 395 - .into_response(); 336 + return ApiError::EmailTaken.into_response(); 396 337 } 397 338 398 339 if let Err(e) = request_channel_verification( ··· 405 346 ) 406 347 .await 407 348 { 408 - return ( 409 - StatusCode::INTERNAL_SERVER_ERROR, 410 - Json(json!({"error": "InternalError", "message": e})), 411 - ) 412 - .into_response(); 349 + return ApiError::InternalError(Some(e)).into_response(); 413 350 } 414 351 verification_required.push("email".to_string()); 415 352 info!(did = %user.did, "Requested email verification"); ··· 425 362 .execute(&state.db) 426 363 .await 427 364 { 428 - return ( 429 - StatusCode::INTERNAL_SERVER_ERROR, 430 - Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 431 - ) 432 - .into_response(); 365 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 433 366 } 434 367 info!(did = %user.did, "Cleared Discord ID"); 435 368 } else { ··· 438 371 ) 439 372 .await 440 373 { 441 - return ( 442 - StatusCode::INTERNAL_SERVER_ERROR, 443 - Json(json!({"error": "InternalError", "message": e})), 444 - ) 445 - .into_response(); 374 + return ApiError::InternalError(Some(e)).into_response(); 446 375 } 447 376 verification_required.push("discord".to_string()); 448 377 info!(did = %user.did, "Requested Discord verification"); ··· 459 388 .execute(&state.db) 460 389 .await 461 390 { 462 - return ( 463 - StatusCode::INTERNAL_SERVER_ERROR, 464 - Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 465 - ) 466 - .into_response(); 391 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 467 392 } 468 393 info!(did = %user.did, "Cleared Telegram username"); 469 394 } else { ··· 477 402 ) 478 403 .await 479 404 { 480 - return ( 481 - StatusCode::INTERNAL_SERVER_ERROR, 482 - Json(json!({"error": "InternalError", "message": e})), 483 - ) 484 - .into_response(); 405 + return ApiError::InternalError(Some(e)).into_response(); 485 406 } 486 407 verification_required.push("telegram".to_string()); 487 408 info!(did = %user.did, "Requested Telegram verification"); ··· 497 418 .execute(&state.db) 498 419 .await 499 420 { 500 - return ( 501 - StatusCode::INTERNAL_SERVER_ERROR, 502 - Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 503 - ) 504 - .into_response(); 421 + return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 505 422 } 506 423 info!(did = %user.did, "Cleared Signal number"); 507 424 } else { ··· 509 426 request_channel_verification(&state.db, user_id, &user.did, "signal", signal, None) 510 427 .await 511 428 { 512 - return ( 513 - StatusCode::INTERNAL_SERVER_ERROR, 514 - Json(json!({"error": "InternalError", "message": e})), 515 - ) 516 - .into_response(); 429 + return ApiError::InternalError(Some(e)).into_response(); 517 430 } 518 431 verification_required.push("signal".to_string()); 519 432 info!(did = %user.did, "Requested Signal verification");
+14 -42
src/api/proxy.rs
··· 1 1 use std::convert::Infallible; 2 2 3 + use crate::api::error::ApiError; 3 4 use crate::api::proxy_client::proxy_client; 4 5 use crate::state::AppState; 5 6 use axum::{ 6 - Json, 7 7 body::Bytes, 8 8 extract::{RawQuery, Request, State}, 9 9 handler::Handler, ··· 11 11 response::{IntoResponse, Response}, 12 12 }; 13 13 use futures_util::future::Either; 14 - use serde_json::json; 15 14 use tower::{Service, util::BoxCloneSyncService}; 16 15 use tracing::{error, info, warn}; 17 16 ··· 120 119 let method = uri.path().trim_start_matches("/"); 121 120 if is_protected_method(&method) { 122 121 warn!(method = %method, "Attempted to proxy protected method"); 123 - return ( 124 - StatusCode::BAD_REQUEST, 125 - Json(json!({ 126 - "error": "InvalidRequest", 127 - "message": format!("Cannot proxy protected method: {}", method) 128 - })), 129 - ) 122 + return ApiError::InvalidRequest(format!("Cannot proxy protected method: {}", method)) 130 123 .into_response(); 131 124 } 132 125 133 - let proxy_header = match headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) { 134 - Some(h) => h.to_string(), 135 - None => { 136 - return ( 137 - StatusCode::BAD_REQUEST, 138 - Json(json!({ 139 - "error": "InvalidRequest", 140 - "message": "Missing required atproto-proxy header" 141 - })), 142 - ) 143 - .into_response(); 144 - } 126 + let Some(proxy_header) = headers 127 + .get("atproto-proxy") 128 + .and_then(|h| h.to_str().ok()) 129 + .map(String::from) 130 + else { 131 + return ApiError::InvalidRequest("Missing required atproto-proxy header".into()) 132 + .into_response(); 145 133 }; 146 134 147 135 let did = proxy_header.split('#').next().unwrap_or(&proxy_header); 148 - let resolved = match state.did_resolver.resolve_did(did).await { 149 - Some(r) => r, 150 - None => { 151 - error!(did = %did, "Could not resolve service DID"); 152 - return ( 153 - StatusCode::BAD_GATEWAY, 154 - Json(json!({ 155 - "error": "UpstreamFailure", 156 - "message": "Could not resolve service DID" 157 - })), 158 - ) 159 - .into_response(); 160 - } 136 + let Some(resolved) = state.did_resolver.resolve_did(did).await else { 137 + error!(did = %did, "Could not resolve service DID"); 138 + return ApiError::UpstreamFailure.into_response(); 161 139 }; 162 140 163 141 let target_url = match &query { ··· 220 198 "{} error=\"invalid_token\", error_description=\"Token has expired\"", 221 199 scheme 222 200 ); 223 - let mut response = ( 224 - StatusCode::UNAUTHORIZED, 225 - Json(json!({ 226 - "error": "ExpiredToken", 227 - "message": "Token has expired" 228 - })), 229 - ) 230 - .into_response(); 201 + let mut response = 202 + ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); 231 203 response 232 204 .headers_mut() 233 205 .insert("WWW-Authenticate", www_auth.parse().unwrap());
+33 -97
src/api/repo/blob.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::auth::{ServiceTokenVerifier, is_service_token}; 2 3 use crate::delegation::{self, DelegationActionType}; 3 4 use crate::state::AppState; ··· 23 24 headers: axum::http::HeaderMap, 24 25 body: Body, 25 26 ) -> Response { 26 - let token = match crate::auth::extract_bearer_token_from_header( 27 + let Some(token) = crate::auth::extract_bearer_token_from_header( 27 28 headers.get("Authorization").and_then(|h| h.to_str().ok()), 28 - ) { 29 - Some(t) => t, 30 - None => { 31 - return ( 32 - StatusCode::UNAUTHORIZED, 33 - Json(json!({"error": "AuthenticationRequired"})), 34 - ) 35 - .into_response(); 36 - } 29 + ) else { 30 + return ApiError::AuthenticationRequired.into_response(); 37 31 }; 38 32 39 33 let is_service_auth = is_service_token(&token); ··· 51 45 } 52 46 Err(e) => { 53 47 error!("Service token verification failed: {:?}", e); 54 - return ( 55 - StatusCode::UNAUTHORIZED, 56 - Json(json!({"error": "AuthenticationFailed", "message": format!("Service token verification failed: {}", e)})), 57 - ) 58 - .into_response(); 48 + return ApiError::AuthenticationFailed(Some(format!( 49 + "Service token verification failed: {}", 50 + e 51 + ))) 52 + .into_response(); 59 53 } 60 54 } 61 55 } else { ··· 74 68 } 75 69 let deactivated = sqlx::query_scalar!( 76 70 "SELECT deactivated_at FROM users WHERE did = $1", 77 - user.did 71 + &user.did 78 72 ) 79 73 .fetch_optional(&state.db) 80 74 .await 81 75 .ok() 82 76 .flatten() 83 77 .flatten(); 84 - let ctrl_did = user.controller_did.clone(); 85 - (user.did, deactivated.is_some(), ctrl_did) 78 + let ctrl_did = user.controller_did.map(|d| d.to_string()); 79 + (user.did.to_string(), deactivated.is_some(), ctrl_did) 86 80 } 87 81 Err(_) => { 88 - return ( 89 - StatusCode::UNAUTHORIZED, 90 - Json(json!({"error": "AuthenticationFailed"})), 91 - ) 92 - .into_response(); 82 + return ApiError::AuthenticationFailed(None).into_response(); 93 83 } 94 84 } 95 85 }; ··· 98 88 .await 99 89 .unwrap_or(false) 100 90 { 101 - return ( 102 - StatusCode::FORBIDDEN, 103 - Json(json!({ 104 - "error": "AccountMigrated", 105 - "message": "Account has been migrated to another PDS. Blob operations are not allowed." 106 - })), 107 - ) 108 - .into_response(); 91 + return ApiError::Forbidden.into_response(); 109 92 } 110 93 111 94 let mime_type = headers ··· 120 103 let user_id = match user_query { 121 104 Ok(Some(row)) => row.id, 122 105 _ => { 123 - return ( 124 - StatusCode::INTERNAL_SERVER_ERROR, 125 - Json(json!({"error": "InternalError"})), 126 - ) 127 - .into_response(); 106 + return ApiError::InternalError(None).into_response(); 128 107 } 129 108 }; 130 109 ··· 143 122 Ok(result) => result, 144 123 Err(e) => { 145 124 error!("Failed to stream blob to storage: {:?}", e); 146 - return ( 147 - StatusCode::INTERNAL_SERVER_ERROR, 148 - Json(json!({"error": "InternalError", "message": "Failed to store blob"})), 149 - ) 150 - .into_response(); 125 + return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 151 126 } 152 127 }; 153 128 154 129 let size = upload_result.size; 155 130 if size > max_size { 156 131 let _ = state.blob_store.delete(&temp_key).await; 157 - return ( 158 - StatusCode::PAYLOAD_TOO_LARGE, 159 - Json(json!({"error": "BlobTooLarge", "message": format!("Blob size {} exceeds maximum of {} bytes", size, max_size)})), 160 - ) 161 - .into_response(); 132 + return ApiError::InvalidRequest(format!( 133 + "Blob size {} exceeds maximum of {} bytes", 134 + size, max_size 135 + )) 136 + .into_response(); 162 137 } 163 138 164 139 let multihash = match Multihash::wrap(0x12, &upload_result.sha256_hash) { ··· 166 141 Err(e) => { 167 142 let _ = state.blob_store.delete(&temp_key).await; 168 143 error!("Failed to create multihash for blob: {:?}", e); 169 - return ( 170 - StatusCode::INTERNAL_SERVER_ERROR, 171 - Json(json!({"error": "InternalError", "message": "Failed to hash blob"})), 172 - ) 173 - .into_response(); 144 + return ApiError::InternalError(Some("Failed to hash blob".into())).into_response(); 174 145 } 175 146 }; 176 147 let cid = Cid::new_v1(0x55, multihash); ··· 187 158 Err(e) => { 188 159 let _ = state.blob_store.delete(&temp_key).await; 189 160 error!("Failed to begin transaction: {:?}", e); 190 - return ( 191 - StatusCode::INTERNAL_SERVER_ERROR, 192 - Json(json!({"error": "InternalError"})), 193 - ) 194 - .into_response(); 161 + return ApiError::InternalError(None).into_response(); 195 162 } 196 163 }; 197 164 ··· 212 179 Err(e) => { 213 180 let _ = state.blob_store.delete(&temp_key).await; 214 181 error!("Failed to insert blob record: {:?}", e); 215 - return ( 216 - StatusCode::INTERNAL_SERVER_ERROR, 217 - Json(json!({"error": "InternalError"})), 218 - ) 219 - .into_response(); 182 + return ApiError::InternalError(None).into_response(); 220 183 } 221 184 }; 222 185 223 186 if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 224 187 let _ = state.blob_store.delete(&temp_key).await; 225 188 error!("Failed to copy blob to final location: {:?}", e); 226 - return ( 227 - StatusCode::INTERNAL_SERVER_ERROR, 228 - Json(json!({"error": "InternalError", "message": "Failed to store blob"})), 229 - ) 230 - .into_response(); 189 + return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 231 190 } 232 191 233 192 let _ = state.blob_store.delete(&temp_key).await; ··· 240 199 storage_key, cleanup_err 241 200 ); 242 201 } 243 - return ( 244 - StatusCode::INTERNAL_SERVER_ERROR, 245 - Json(json!({"error": "InternalError"})), 246 - ) 247 - .into_response(); 202 + return ApiError::InternalError(None).into_response(); 248 203 } 249 204 250 205 if let Some(ref controller) = controller_did { ··· 303 258 headers: axum::http::HeaderMap, 304 259 Query(params): Query<ListMissingBlobsParams>, 305 260 ) -> Response { 306 - let token = match crate::auth::extract_bearer_token_from_header( 261 + let Some(token) = crate::auth::extract_bearer_token_from_header( 307 262 headers.get("Authorization").and_then(|h| h.to_str().ok()), 308 - ) { 309 - Some(t) => t, 310 - None => { 311 - return ( 312 - StatusCode::UNAUTHORIZED, 313 - Json(json!({"error": "AuthenticationRequired"})), 314 - ) 315 - .into_response(); 316 - } 263 + ) else { 264 + return ApiError::AuthenticationRequired.into_response(); 317 265 }; 318 266 let auth_user = 319 267 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 320 268 Ok(user) => user, 321 269 Err(_) => { 322 - return ( 323 - StatusCode::UNAUTHORIZED, 324 - Json(json!({"error": "AuthenticationFailed"})), 325 - ) 326 - .into_response(); 270 + return ApiError::AuthenticationFailed(None).into_response(); 327 271 } 328 272 }; 329 273 let did = auth_user.did; 330 - let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 274 + let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did.as_str()) 331 275 .fetch_optional(&state.db) 332 276 .await; 333 277 let user_id = match user_query { 334 278 Ok(Some(row)) => row.id, 335 279 _ => { 336 - return ( 337 - StatusCode::INTERNAL_SERVER_ERROR, 338 - Json(json!({"error": "InternalError"})), 339 - ) 340 - .into_response(); 280 + return ApiError::InternalError(None).into_response(); 341 281 } 342 282 }; 343 283 let limit = params.limit.unwrap_or(500).clamp(1, 1000); ··· 361 301 Ok(r) => r, 362 302 Err(e) => { 363 303 error!("DB error fetching missing blobs: {:?}", e); 364 - return ( 365 - StatusCode::INTERNAL_SERVER_ERROR, 366 - Json(json!({"error": "InternalError"})), 367 - ) 368 - .into_response(); 304 + return ApiError::InternalError(None).into_response(); 369 305 } 370 306 }; 371 307 let has_more = rows.len() > limit as usize;
+83 -269
src/api/repo/import.rs
··· 1 - use crate::api::ApiError; 1 + use crate::api::error::ApiError; 2 2 use crate::api::repo::record::create_signed_commit; 3 + use crate::api::EmptyResponse; 3 4 use crate::state::AppState; 4 5 use crate::sync::import::{ImportError, apply_import, parse_car}; 5 6 use crate::sync::verify::CarVerifier; 6 7 use axum::{ 7 - Json, 8 8 body::Bytes, 9 9 extract::State, 10 - http::StatusCode, 11 10 response::{IntoResponse, Response}, 12 11 }; 13 12 use jacquard::types::{integer::LimitedU32, string::Tid}; ··· 28 27 .map(|v| v != "false" && v != "0") 29 28 .unwrap_or(true); 30 29 if !accepting_imports { 31 - return ( 32 - StatusCode::BAD_REQUEST, 33 - Json(json!({ 34 - "error": "InvalidRequest", 35 - "message": "Service is not accepting repo imports" 36 - })), 37 - ) 30 + return ApiError::InvalidRequest("Service is not accepting repo imports".into()) 38 31 .into_response(); 39 32 } 40 33 let max_size: usize = std::env::var("MAX_IMPORT_SIZE") ··· 42 35 .and_then(|s| s.parse().ok()) 43 36 .unwrap_or(DEFAULT_MAX_IMPORT_SIZE); 44 37 if body.len() > max_size { 45 - return ( 46 - StatusCode::PAYLOAD_TOO_LARGE, 47 - Json(json!({ 48 - "error": "InvalidRequest", 49 - "message": format!("Import size exceeds limit of {} bytes", max_size) 50 - })), 51 - ) 52 - .into_response(); 38 + return ApiError::PayloadTooLarge(format!( 39 + "Import size exceeds limit of {} bytes", 40 + max_size 41 + )) 42 + .into_response(); 53 43 } 54 44 let token = match crate::auth::extract_bearer_token_from_header( 55 45 headers.get("Authorization").and_then(|h| h.to_str().ok()), ··· 72 62 { 73 63 Ok(Some(row)) => row, 74 64 Ok(None) => { 75 - return ( 76 - StatusCode::NOT_FOUND, 77 - Json(json!({"error": "AccountNotFound"})), 78 - ) 79 - .into_response(); 65 + return ApiError::AccountNotFound.into_response(); 80 66 } 81 67 Err(e) => { 82 68 error!("DB error fetching user: {:?}", e); 83 - return ( 84 - StatusCode::INTERNAL_SERVER_ERROR, 85 - Json(json!({"error": "InternalError"})), 86 - ) 87 - .into_response(); 69 + return ApiError::InternalError(None).into_response(); 88 70 } 89 71 }; 90 72 if user.takedown_ref.is_some() { 91 - return ( 92 - StatusCode::FORBIDDEN, 93 - Json(json!({ 94 - "error": "AccountTakenDown", 95 - "message": "Account has been taken down" 96 - })), 97 - ) 98 - .into_response(); 73 + return ApiError::AccountTakedown.into_response(); 99 74 } 100 75 let user_id = user.id; 101 76 let (root, blocks) = match parse_car(&body).await { 102 77 Ok((r, b)) => (r, b), 103 78 Err(ImportError::InvalidRootCount) => { 104 - return ( 105 - StatusCode::BAD_REQUEST, 106 - Json(json!({ 107 - "error": "InvalidRequest", 108 - "message": "Expected exactly one root in CAR file" 109 - })), 110 - ) 79 + return ApiError::InvalidRequest("Expected exactly one root in CAR file".into()) 111 80 .into_response(); 112 81 } 113 82 Err(ImportError::CarParse(msg)) => { 114 - return ( 115 - StatusCode::BAD_REQUEST, 116 - Json(json!({ 117 - "error": "InvalidRequest", 118 - "message": format!("Failed to parse CAR file: {}", msg) 119 - })), 120 - ) 83 + return ApiError::InvalidRequest(format!("Failed to parse CAR file: {}", msg)) 121 84 .into_response(); 122 85 } 123 86 Err(e) => { 124 87 error!("CAR parsing error: {:?}", e); 125 - return ( 126 - StatusCode::BAD_REQUEST, 127 - Json(json!({ 128 - "error": "InvalidRequest", 129 - "message": format!("Invalid CAR file: {}", e) 130 - })), 131 - ) 132 - .into_response(); 88 + return ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)).into_response(); 133 89 } 134 90 }; 135 91 info!( ··· 138 94 blocks.len(), 139 95 root 140 96 ); 141 - let root_block = match blocks.get(&root) { 142 - Some(b) => b, 143 - None => { 144 - return ( 145 - StatusCode::BAD_REQUEST, 146 - Json(json!({ 147 - "error": "InvalidRequest", 148 - "message": "Root block not found in CAR file" 149 - })), 150 - ) 151 - .into_response(); 152 - } 97 + let Some(root_block) = blocks.get(&root) else { 98 + return ApiError::InvalidRequest("Root block not found in CAR file".into()).into_response(); 153 99 }; 154 100 let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { 155 101 Ok(commit) => commit.did().to_string(), 156 102 Err(e) => { 157 - return ( 158 - StatusCode::BAD_REQUEST, 159 - Json(json!({ 160 - "error": "InvalidRequest", 161 - "message": format!("Invalid commit: {}", e) 162 - })), 163 - ) 164 - .into_response(); 103 + return ApiError::InvalidRequest(format!("Invalid commit: {}", e)).into_response(); 165 104 } 166 105 }; 167 106 if commit_did != *did { 168 - return ( 169 - StatusCode::FORBIDDEN, 170 - Json(json!({ 171 - "error": "InvalidRequest", 172 - "message": format!( 173 - "CAR file is for DID {} but you are authenticated as {}", 174 - commit_did, did 175 - ) 176 - })), 177 - ) 178 - .into_response(); 107 + return ApiError::InvalidRepo(format!( 108 + "CAR file is for DID {} but you are authenticated as {}", 109 + commit_did, did 110 + )) 111 + .into_response(); 179 112 } 180 113 let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") 181 114 .map(|v| v == "true" || v == "1") ··· 197 130 commit_did, 198 131 expected_did, 199 132 }) => { 200 - return ( 201 - StatusCode::FORBIDDEN, 202 - Json(json!({ 203 - "error": "InvalidRequest", 204 - "message": format!( 205 - "CAR file is for DID {} but you are authenticated as {}", 206 - commit_did, expected_did 207 - ) 208 - })), 209 - ) 210 - .into_response(); 133 + return ApiError::InvalidRepo(format!( 134 + "CAR file is for DID {} but you are authenticated as {}", 135 + commit_did, expected_did 136 + )) 137 + .into_response(); 211 138 } 212 139 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 213 - return ( 214 - StatusCode::BAD_REQUEST, 215 - Json(json!({ 216 - "error": "InvalidRequest", 217 - "message": format!("MST validation failed: {}", msg) 218 - })), 219 - ) 140 + return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 220 141 .into_response(); 221 142 } 222 143 Err(e) => { 223 144 error!("CAR structure verification error: {:?}", e); 224 - return ( 225 - StatusCode::BAD_REQUEST, 226 - Json(json!({ 227 - "error": "InvalidRequest", 228 - "message": format!("CAR verification failed: {}", e) 229 - })), 230 - ) 145 + return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 231 146 .into_response(); 232 147 } 233 148 } ··· 245 160 commit_did, 246 161 expected_did, 247 162 }) => { 248 - return ( 249 - StatusCode::FORBIDDEN, 250 - Json(json!({ 251 - "error": "InvalidRequest", 252 - "message": format!( 253 - "CAR file is for DID {} but you are authenticated as {}", 254 - commit_did, expected_did 255 - ) 256 - })), 257 - ) 258 - .into_response(); 163 + return ApiError::InvalidRepo(format!( 164 + "CAR file is for DID {} but you are authenticated as {}", 165 + commit_did, expected_did 166 + )) 167 + .into_response(); 259 168 } 260 169 Err(crate::sync::verify::VerifyError::InvalidSignature) => { 261 - return ( 262 - StatusCode::BAD_REQUEST, 263 - Json(json!({ 264 - "error": "InvalidSignature", 265 - "message": "CAR file commit signature verification failed" 266 - })), 170 + return ApiError::InvalidRequest( 171 + "CAR file commit signature verification failed".into(), 267 172 ) 268 - .into_response(); 173 + .into_response(); 269 174 } 270 175 Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { 271 176 warn!("DID resolution failed during import verification: {}", msg); 272 - return ( 273 - StatusCode::BAD_REQUEST, 274 - Json(json!({ 275 - "error": "InvalidRequest", 276 - "message": format!("Failed to verify DID: {}", msg) 277 - })), 278 - ) 177 + return ApiError::InvalidRequest(format!("Failed to verify DID: {}", msg)) 279 178 .into_response(); 280 179 } 281 180 Err(crate::sync::verify::VerifyError::NoSigningKey) => { 282 - return ( 283 - StatusCode::BAD_REQUEST, 284 - Json(json!({ 285 - "error": "InvalidRequest", 286 - "message": "DID document does not contain a signing key" 287 - })), 181 + return ApiError::InvalidRequest( 182 + "DID document does not contain a signing key".into(), 288 183 ) 289 - .into_response(); 184 + .into_response(); 290 185 } 291 186 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 292 - return ( 293 - StatusCode::BAD_REQUEST, 294 - Json(json!({ 295 - "error": "InvalidRequest", 296 - "message": format!("MST validation failed: {}", msg) 297 - })), 298 - ) 187 + return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 299 188 .into_response(); 300 189 } 301 190 Err(e) => { 302 191 error!("CAR verification error: {:?}", e); 303 - return ( 304 - StatusCode::BAD_REQUEST, 305 - Json(json!({ 306 - "error": "InvalidRequest", 307 - "message": format!("CAR verification failed: {}", e) 308 - })), 309 - ) 192 + return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 310 193 .into_response(); 311 194 } 312 195 } ··· 364 247 Ok(Some(row)) => row, 365 248 Ok(None) => { 366 249 error!("No signing key found for user {}", did); 367 - return ( 368 - StatusCode::INTERNAL_SERVER_ERROR, 369 - Json(json!({"error": "InternalError", "message": "Signing key not found"})), 370 - ) 250 + return ApiError::InternalError(Some("Signing key not found".into())) 371 251 .into_response(); 372 252 } 373 253 Err(e) => { 374 254 error!("DB error fetching signing key: {:?}", e); 375 - return ( 376 - StatusCode::INTERNAL_SERVER_ERROR, 377 - Json(json!({"error": "InternalError"})), 378 - ) 379 - .into_response(); 255 + return ApiError::InternalError(None).into_response(); 380 256 } 381 257 }; 382 258 let key_bytes = ··· 384 260 Ok(k) => k, 385 261 Err(e) => { 386 262 error!("Failed to decrypt signing key: {}", e); 387 - return ( 388 - StatusCode::INTERNAL_SERVER_ERROR, 389 - Json(json!({"error": "InternalError"})), 390 - ) 391 - .into_response(); 263 + return ApiError::InternalError(None).into_response(); 392 264 } 393 265 }; 394 266 let signing_key = match SigningKey::from_slice(&key_bytes) { 395 267 Ok(k) => k, 396 268 Err(e) => { 397 269 error!("Invalid signing key: {:?}", e); 398 - return ( 399 - StatusCode::INTERNAL_SERVER_ERROR, 400 - Json(json!({"error": "InternalError"})), 401 - ) 402 - .into_response(); 270 + return ApiError::InternalError(None).into_response(); 403 271 } 404 272 }; 405 273 let new_rev = Tid::now(LimitedU32::MIN); ··· 414 282 Ok(result) => result, 415 283 Err(e) => { 416 284 error!("Failed to create new commit: {}", e); 417 - return ( 418 - StatusCode::INTERNAL_SERVER_ERROR, 419 - Json(json!({"error": "InternalError"})), 420 - ) 421 - .into_response(); 285 + return ApiError::InternalError(None).into_response(); 422 286 } 423 287 }; 424 288 let new_root_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 425 289 Ok(cid) => cid, 426 290 Err(e) => { 427 291 error!("Failed to store new commit block: {:?}", e); 428 - return ( 429 - StatusCode::INTERNAL_SERVER_ERROR, 430 - Json(json!({"error": "InternalError"})), 431 - ) 432 - .into_response(); 292 + return ApiError::InternalError(None).into_response(); 433 293 } 434 294 }; 435 295 let new_root_str = new_root_cid.to_string(); ··· 443 303 .await 444 304 { 445 305 error!("Failed to update repo root: {:?}", e); 446 - return ( 447 - StatusCode::INTERNAL_SERVER_ERROR, 448 - Json(json!({"error": "InternalError"})), 449 - ) 450 - .into_response(); 306 + return ApiError::InternalError(None).into_response(); 451 307 } 452 308 let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect(); 453 309 all_block_cids.push(new_root_cid.to_bytes()); ··· 464 320 .await 465 321 { 466 322 error!("Failed to insert user_blocks: {:?}", e); 467 - return ( 468 - StatusCode::INTERNAL_SERVER_ERROR, 469 - Json(json!({"error": "InternalError"})), 470 - ) 471 - .into_response(); 323 + return ApiError::InternalError(None).into_response(); 472 324 } 473 325 info!( 474 326 "Created new commit for imported repo: cid={}, rev={}", ··· 499 351 ); 500 352 } 501 353 } 502 - (StatusCode::OK, Json(json!({}))).into_response() 354 + EmptyResponse::ok().into_response() 503 355 } 504 - Err(ImportError::SizeLimitExceeded) => ( 505 - StatusCode::BAD_REQUEST, 506 - Json(json!({ 507 - "error": "InvalidRequest", 508 - "message": format!("Import exceeds block limit of {}", max_blocks) 509 - })), 510 - ) 511 - .into_response(), 512 - Err(ImportError::RepoNotFound) => ( 513 - StatusCode::NOT_FOUND, 514 - Json(json!({ 515 - "error": "RepoNotFound", 516 - "message": "Repository not initialized for this account" 517 - })), 518 - ) 519 - .into_response(), 520 - Err(ImportError::InvalidCbor(msg)) => ( 521 - StatusCode::BAD_REQUEST, 522 - Json(json!({ 523 - "error": "InvalidRequest", 524 - "message": format!("Invalid CBOR data: {}", msg) 525 - })), 526 - ) 527 - .into_response(), 528 - Err(ImportError::InvalidCommit(msg)) => ( 529 - StatusCode::BAD_REQUEST, 530 - Json(json!({ 531 - "error": "InvalidRequest", 532 - "message": format!("Invalid commit structure: {}", msg) 533 - })), 534 - ) 535 - .into_response(), 536 - Err(ImportError::BlockNotFound(cid)) => ( 537 - StatusCode::BAD_REQUEST, 538 - Json(json!({ 539 - "error": "InvalidRequest", 540 - "message": format!("Referenced block not found in CAR: {}", cid) 541 - })), 542 - ) 543 - .into_response(), 544 - Err(ImportError::ConcurrentModification) => ( 545 - StatusCode::CONFLICT, 546 - Json(json!({ 547 - "error": "ConcurrentModification", 548 - "message": "Repository is being modified by another operation, please retry" 549 - })), 550 - ) 551 - .into_response(), 552 - Err(ImportError::VerificationFailed(ve)) => ( 553 - StatusCode::BAD_REQUEST, 554 - Json(json!({ 555 - "error": "VerificationFailed", 556 - "message": format!("CAR verification failed: {}", ve) 557 - })), 558 - ) 559 - .into_response(), 560 - Err(ImportError::DidMismatch { car_did, auth_did }) => ( 561 - StatusCode::FORBIDDEN, 562 - Json(json!({ 563 - "error": "DidMismatch", 564 - "message": format!("CAR is for {} but authenticated as {}", car_did, auth_did) 565 - })), 566 - ) 567 - .into_response(), 356 + Err(ImportError::SizeLimitExceeded) => { 357 + ApiError::PayloadTooLarge(format!("Import exceeds block limit of {}", max_blocks)) 358 + .into_response() 359 + } 360 + Err(ImportError::RepoNotFound) => { 361 + ApiError::RepoNotFound(Some("Repository not initialized for this account".into())) 362 + .into_response() 363 + } 364 + Err(ImportError::InvalidCbor(msg)) => { 365 + ApiError::InvalidRequest(format!("Invalid CBOR data: {}", msg)).into_response() 366 + } 367 + Err(ImportError::InvalidCommit(msg)) => { 368 + ApiError::InvalidRequest(format!("Invalid commit structure: {}", msg)).into_response() 369 + } 370 + Err(ImportError::BlockNotFound(cid)) => { 371 + ApiError::InvalidRequest(format!("Referenced block not found in CAR: {}", cid)) 372 + .into_response() 373 + } 374 + Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some("Repository is being modified by another operation, please retry".into(),)) 375 + .into_response(), 376 + Err(ImportError::VerificationFailed(ve)) => { 377 + ApiError::InvalidRequest(format!("CAR verification failed: {}", ve)).into_response() 378 + } 379 + Err(ImportError::DidMismatch { car_did, auth_did }) => { 380 + ApiError::InvalidRequest(format!( 381 + "CAR is for {} but authenticated as {}", 382 + car_did, auth_did 383 + )) 384 + .into_response() 385 + } 568 386 Err(e) => { 569 387 error!("Import error: {:?}", e); 570 - ( 571 - StatusCode::INTERNAL_SERVER_ERROR, 572 - Json(json!({"error": "InternalError"})), 573 - ) 574 - .into_response() 388 + ApiError::InternalError(None).into_response() 575 389 } 576 390 } 577 391 }
+11 -17
src/api/repo/meta.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 3 + use crate::types::AtIdentifier; 2 4 use axum::{ 3 5 Json, 4 6 extract::{Query, State}, 5 - http::StatusCode, 6 7 response::{IntoResponse, Response}, 7 8 }; 8 9 use serde::Deserialize; ··· 10 11 11 12 #[derive(Deserialize)] 12 13 pub struct DescribeRepoInput { 13 - pub repo: String, 14 + pub repo: AtIdentifier, 14 15 } 15 16 16 17 pub async fn describe_repo( ··· 18 19 Query(input): Query<DescribeRepoInput>, 19 20 ) -> Response { 20 21 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 21 - let user_row = if input.repo.starts_with("did:") { 22 + let user_row = if input.repo.is_did() { 22 23 sqlx::query!( 23 24 "SELECT id, handle, did FROM users WHERE did = $1", 24 - input.repo 25 + input.repo.as_str() 25 26 ) 26 27 .fetch_optional(&state.db) 27 28 .await 28 29 .map(|opt| opt.map(|r| (r.id, r.handle, r.did))) 29 30 } else { 30 - let handle = if !input.repo.contains('.') { 31 - format!("{}.{}", input.repo, hostname) 31 + let repo_str = input.repo.as_str(); 32 + let handle = if !repo_str.contains('.') { 33 + format!("{}.{}", repo_str, hostname) 32 34 } else { 33 - input.repo.clone() 35 + repo_str.to_string() 34 36 }; 35 37 sqlx::query!( 36 38 "SELECT id, handle, did FROM users WHERE handle = $1", ··· 43 45 let (user_id, handle, did) = match user_row { 44 46 Ok(Some((id, handle, did))) => (id, handle, did), 45 47 Ok(None) => { 46 - return ( 47 - StatusCode::NOT_FOUND, 48 - Json(json!({"error": "RepoNotFound", "message": "Repo not found"})), 49 - ) 50 - .into_response(); 48 + return ApiError::RepoNotFound(Some("Repo not found".into())).into_response(); 51 49 } 52 50 Err(_) => { 53 - return ( 54 - StatusCode::INTERNAL_SERVER_ERROR, 55 - Json(json!({"error": "InternalError"})), 56 - ) 57 - .into_response(); 51 + return ApiError::InternalError(None).into_response(); 58 52 } 59 53 }; 60 54 let collections_query = sqlx::query!(
+79 -184
src/api/repo/record/batch.rs
··· 1 1 use super::validation::validate_record_with_status; 2 2 use super::write::has_verified_comms_channel; 3 + use crate::api::error::ApiError; 3 4 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 5 use crate::delegation::{self, DelegationActionType}; 5 6 use crate::repo::tracking::TrackingBlockStore; 6 7 use crate::state::AppState; 7 - use crate::validation::ValidationStatus; 8 + use crate::types::{AtIdentifier, AtUri, Nsid, Rkey}; 8 9 use axum::{ 9 10 Json, 10 11 extract::State, ··· 12 13 response::{IntoResponse, Response}, 13 14 }; 14 15 use cid::Cid; 15 - use jacquard::types::{ 16 - integer::LimitedU32, 17 - string::{Nsid, Tid}, 18 - }; 19 16 use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 20 17 use serde::{Deserialize, Serialize}; 21 18 use serde_json::json; ··· 30 27 pub enum WriteOp { 31 28 #[serde(rename = "com.atproto.repo.applyWrites#create")] 32 29 Create { 33 - collection: String, 34 - rkey: Option<String>, 30 + collection: Nsid, 31 + rkey: Option<Rkey>, 35 32 value: serde_json::Value, 36 33 }, 37 34 #[serde(rename = "com.atproto.repo.applyWrites#update")] 38 35 Update { 39 - collection: String, 40 - rkey: String, 36 + collection: Nsid, 37 + rkey: Rkey, 41 38 value: serde_json::Value, 42 39 }, 43 40 #[serde(rename = "com.atproto.repo.applyWrites#delete")] 44 - Delete { collection: String, rkey: String }, 41 + Delete { collection: Nsid, rkey: Rkey }, 45 42 } 46 43 47 44 #[derive(Deserialize)] 48 45 #[serde(rename_all = "camelCase")] 49 46 pub struct ApplyWritesInput { 50 - pub repo: String, 47 + pub repo: AtIdentifier, 51 48 pub validate: Option<bool>, 52 49 pub writes: Vec<WriteOp>, 53 50 pub swap_commit: Option<String>, ··· 58 55 pub enum WriteResult { 59 56 #[serde(rename = "com.atproto.repo.applyWrites#createResult")] 60 57 CreateResult { 61 - uri: String, 58 + uri: AtUri, 62 59 cid: String, 63 60 #[serde(rename = "validationStatus", skip_serializing_if = "Option::is_none")] 64 61 validation_status: Option<String>, 65 62 }, 66 63 #[serde(rename = "com.atproto.repo.applyWrites#updateResult")] 67 64 UpdateResult { 68 - uri: String, 65 + uri: AtUri, 69 66 cid: String, 70 67 #[serde(rename = "validationStatus", skip_serializing_if = "Option::is_none")] 71 68 validation_status: Option<String>, ··· 96 93 input.repo, 97 94 input.writes.len() 98 95 ); 99 - let token = match crate::auth::extract_bearer_token_from_header( 96 + let Some(token) = crate::auth::extract_bearer_token_from_header( 100 97 headers.get("Authorization").and_then(|h| h.to_str().ok()), 101 - ) { 102 - Some(t) => t, 103 - None => { 104 - return ( 105 - StatusCode::UNAUTHORIZED, 106 - Json(json!({"error": "AuthenticationRequired"})), 107 - ) 108 - .into_response(); 109 - } 98 + ) else { 99 + return ApiError::AuthenticationRequired.into_response(); 110 100 }; 111 101 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await { 112 102 Ok(user) => user, 113 - Err(_) => { 114 - return ( 115 - StatusCode::UNAUTHORIZED, 116 - Json(json!({"error": "AuthenticationFailed"})), 117 - ) 118 - .into_response(); 119 - } 103 + Err(_) => return ApiError::AuthenticationFailed(None).into_response(), 120 104 }; 121 105 let did = auth_user.did.clone(); 122 106 let is_oauth = auth_user.is_oauth; 123 107 let scope = auth_user.scope; 124 108 let controller_did = auth_user.controller_did.clone(); 125 - if input.repo != did { 126 - return ( 127 - StatusCode::FORBIDDEN, 128 - Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"})), 129 - ) 109 + if input.repo.as_str() != did { 110 + return ApiError::InvalidRepo("Repo does not match authenticated user".into()) 130 111 .into_response(); 131 112 } 132 113 if crate::util::is_account_migrated(&state.db, &did) 133 114 .await 134 115 .unwrap_or(false) 135 116 { 136 - return ( 137 - StatusCode::FORBIDDEN, 138 - Json(json!({ 139 - "error": "AccountMigrated", 140 - "message": "Account has been migrated to another PDS. Repo operations are not allowed." 141 - })), 142 - ) 143 - .into_response(); 117 + return ApiError::AccountMigrated.into_response(); 144 118 } 145 119 let is_verified = has_verified_comms_channel(&state.db, &did) 146 120 .await ··· 149 123 .await 150 124 .unwrap_or(false); 151 125 if !is_verified && !is_delegated { 152 - return ( 153 - StatusCode::FORBIDDEN, 154 - Json(json!({ 155 - "error": "AccountNotVerified", 156 - "message": "You must verify at least one notification channel (email, Discord, Telegram, or Signal) before creating records" 157 - })), 158 - ) 159 - .into_response(); 126 + return ApiError::AccountNotVerified.into_response(); 160 127 } 161 128 if input.writes.is_empty() { 162 - return ( 163 - StatusCode::BAD_REQUEST, 164 - Json(json!({"error": "InvalidRequest", "message": "writes array is empty"})), 165 - ) 166 - .into_response(); 129 + return ApiError::InvalidRequest("writes array is empty".into()).into_response(); 167 130 } 168 131 if input.writes.len() > MAX_BATCH_WRITES { 169 - return ( 170 - StatusCode::BAD_REQUEST, 171 - Json(json!({"error": "InvalidRequest", "message": format!("Too many writes (max {})", MAX_BATCH_WRITES)})), 172 - ) 132 + return ApiError::InvalidRequest(format!("Too many writes (max {})", MAX_BATCH_WRITES)) 173 133 .into_response(); 174 134 } 175 135 ··· 179 139 .unwrap_or(false); 180 140 if is_oauth || has_custom_scope { 181 141 use std::collections::HashSet; 182 - let create_collections: HashSet<&str> = input 142 + let create_collections: HashSet<&Nsid> = input 183 143 .writes 184 144 .iter() 185 145 .filter_map(|w| { 186 146 if let WriteOp::Create { collection, .. } = w { 187 - Some(collection.as_str()) 147 + Some(collection) 188 148 } else { 189 149 None 190 150 } 191 151 }) 192 152 .collect(); 193 - let update_collections: HashSet<&str> = input 153 + let update_collections: HashSet<&Nsid> = input 194 154 .writes 195 155 .iter() 196 156 .filter_map(|w| { 197 157 if let WriteOp::Update { collection, .. } = w { 198 - Some(collection.as_str()) 158 + Some(collection) 199 159 } else { 200 160 None 201 161 } 202 162 }) 203 163 .collect(); 204 - let delete_collections: HashSet<&str> = input 164 + let delete_collections: HashSet<&Nsid> = input 205 165 .writes 206 166 .iter() 207 167 .filter_map(|w| { 208 168 if let WriteOp::Delete { collection, .. } = w { 209 - Some(collection.as_str()) 169 + Some(collection) 210 170 } else { 211 171 None 212 172 } ··· 245 205 } 246 206 } 247 207 248 - let user_id: uuid::Uuid = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 208 + let user_id: uuid::Uuid = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str()) 249 209 .fetch_optional(&state.db) 250 210 .await 251 211 { 252 212 Ok(Some(id)) => id, 253 - _ => { 254 - return ( 255 - StatusCode::INTERNAL_SERVER_ERROR, 256 - Json(json!({"error": "InternalError", "message": "User not found"})), 257 - ) 258 - .into_response(); 259 - } 213 + _ => return ApiError::InternalError(Some("User not found".into())).into_response(), 260 214 }; 261 215 let root_cid_str: String = match sqlx::query_scalar!( 262 216 "SELECT repo_root_cid FROM repos WHERE user_id = $1", ··· 266 220 .await 267 221 { 268 222 Ok(Some(cid_str)) => cid_str, 269 - _ => { 270 - return ( 271 - StatusCode::INTERNAL_SERVER_ERROR, 272 - Json(json!({"error": "InternalError", "message": "Repo root not found"})), 273 - ) 274 - .into_response(); 275 - } 223 + _ => return ApiError::InternalError(Some("Repo root not found".into())).into_response(), 276 224 }; 277 225 let current_root_cid = match Cid::from_str(&root_cid_str) { 278 226 Ok(c) => c, 279 227 Err(_) => { 280 - return ( 281 - StatusCode::INTERNAL_SERVER_ERROR, 282 - Json(json!({"error": "InternalError", "message": "Invalid repo root CID"})), 283 - ) 284 - .into_response(); 228 + return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response() 285 229 } 286 230 }; 287 231 if let Some(swap_commit) = &input.swap_commit 288 232 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 289 233 { 290 - return ( 291 - StatusCode::CONFLICT, 292 - Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"})), 293 - ) 294 - .into_response(); 234 + return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 295 235 } 296 236 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 297 237 let commit_bytes = match tracking_store.get(&current_root_cid).await { 298 238 Ok(Some(b)) => b, 299 - _ => { 300 - return ( 301 - StatusCode::INTERNAL_SERVER_ERROR, 302 - Json(json!({"error": "InternalError", "message": "Commit block not found"})), 303 - ) 304 - .into_response(); 305 - } 239 + _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 306 240 }; 307 241 let commit = match Commit::from_cbor(&commit_bytes) { 308 242 Ok(c) => c, 309 - _ => { 310 - return ( 311 - StatusCode::INTERNAL_SERVER_ERROR, 312 - Json(json!({"error": "InternalError", "message": "Failed to parse commit"})), 313 - ) 314 - .into_response(); 315 - } 243 + _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 316 244 }; 317 245 let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 318 246 let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); ··· 334 262 match validate_record_with_status( 335 263 value, 336 264 collection, 337 - rkey.as_deref(), 265 + rkey.as_ref().map(|r| r.as_str()), 338 266 require_lexicon, 339 267 ) { 340 268 Ok(status) => Some(status), ··· 342 270 } 343 271 }; 344 272 all_blob_cids.extend(extract_blob_cids(value)); 345 - let rkey = rkey 346 - .clone() 347 - .unwrap_or_else(|| Tid::now(LimitedU32::MIN).to_string()); 273 + let rkey = rkey.clone().unwrap_or_else(Rkey::generate); 348 274 let record_ipld = crate::util::json_to_ipld(value); 349 275 let mut record_bytes = Vec::new(); 350 276 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 351 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response(); 277 + return ApiError::InvalidRecord("Failed to serialize record".into()) 278 + .into_response(); 352 279 } 353 280 let record_cid = match tracking_store.put(&record_bytes).await { 354 281 Ok(c) => c, 355 - Err(_) => return ( 356 - StatusCode::INTERNAL_SERVER_ERROR, 357 - Json( 358 - json!({"error": "InternalError", "message": "Failed to store record"}), 359 - ), 360 - ) 361 - .into_response(), 362 - }; 363 - let collection_nsid = match collection.parse::<Nsid>() { 364 - Ok(n) => n, 365 - Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection", "message": "Invalid collection NSID"}))).into_response(), 282 + Err(_) => { 283 + return ApiError::InternalError(Some("Failed to store record".into())) 284 + .into_response() 285 + } 366 286 }; 367 - let key = format!("{}/{}", collection_nsid, rkey); 287 + let key = format!("{}/{}", collection, rkey); 368 288 modified_keys.push(key.clone()); 369 289 mst = match mst.add(&key, record_cid).await { 370 290 Ok(m) => m, 371 - Err(_) => return ( 372 - StatusCode::INTERNAL_SERVER_ERROR, 373 - Json(json!({"error": "InternalError", "message": "Failed to add to MST"})), 374 - ) 375 - .into_response(), 291 + Err(_) => { 292 + return ApiError::InternalError(Some("Failed to add to MST".into())) 293 + .into_response() 294 + } 376 295 }; 377 - let uri = format!("at://{}/{}/{}", did, collection, rkey); 296 + let uri = AtUri::from_parts(&did, collection, &rkey); 378 297 results.push(WriteResult::CreateResult { 379 298 uri, 380 299 cid: record_cid.to_string(), 381 - validation_status: validation_status.map(|s| match s { 382 - ValidationStatus::Valid => "valid".to_string(), 383 - ValidationStatus::Unknown => "unknown".to_string(), 384 - ValidationStatus::Invalid => "invalid".to_string(), 385 - }), 300 + validation_status: validation_status.map(|s| s.to_string()), 386 301 }); 387 302 ops.push(RecordOp::Create { 388 - collection: collection.clone(), 389 - rkey, 303 + collection: collection.to_string(), 304 + rkey: rkey.to_string(), 390 305 cid: record_cid, 391 306 }); 392 307 } ··· 402 317 match validate_record_with_status( 403 318 value, 404 319 collection, 405 - Some(rkey), 320 + Some(rkey.as_str()), 406 321 require_lexicon, 407 322 ) { 408 323 Ok(status) => Some(status), ··· 413 328 let record_ipld = crate::util::json_to_ipld(value); 414 329 let mut record_bytes = Vec::new(); 415 330 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 416 - return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response(); 331 + return ApiError::InvalidRecord("Failed to serialize record".into()) 332 + .into_response(); 417 333 } 418 334 let record_cid = match tracking_store.put(&record_bytes).await { 419 335 Ok(c) => c, 420 - Err(_) => return ( 421 - StatusCode::INTERNAL_SERVER_ERROR, 422 - Json( 423 - json!({"error": "InternalError", "message": "Failed to store record"}), 424 - ), 425 - ) 426 - .into_response(), 336 + Err(_) => { 337 + return ApiError::InternalError(Some("Failed to store record".into())) 338 + .into_response() 339 + } 427 340 }; 428 - let collection_nsid = match collection.parse::<Nsid>() { 429 - Ok(n) => n, 430 - Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection", "message": "Invalid collection NSID"}))).into_response(), 431 - }; 432 - let key = format!("{}/{}", collection_nsid, rkey); 341 + let key = format!("{}/{}", collection, rkey); 433 342 modified_keys.push(key.clone()); 434 343 let prev_record_cid = mst.get(&key).await.ok().flatten(); 435 344 mst = match mst.update(&key, record_cid).await { 436 345 Ok(m) => m, 437 - Err(_) => return ( 438 - StatusCode::INTERNAL_SERVER_ERROR, 439 - Json(json!({"error": "InternalError", "message": "Failed to update MST"})), 440 - ) 441 - .into_response(), 346 + Err(_) => { 347 + return ApiError::InternalError(Some("Failed to update MST".into())) 348 + .into_response() 349 + } 442 350 }; 443 - let uri = format!("at://{}/{}/{}", did, collection, rkey); 351 + let uri = AtUri::from_parts(&did, collection, rkey); 444 352 results.push(WriteResult::UpdateResult { 445 353 uri, 446 354 cid: record_cid.to_string(), 447 - validation_status: validation_status.map(|s| match s { 448 - ValidationStatus::Valid => "valid".to_string(), 449 - ValidationStatus::Unknown => "unknown".to_string(), 450 - ValidationStatus::Invalid => "invalid".to_string(), 451 - }), 355 + validation_status: validation_status.map(|s| s.to_string()), 452 356 }); 453 357 ops.push(RecordOp::Update { 454 - collection: collection.clone(), 455 - rkey: rkey.clone(), 358 + collection: collection.to_string(), 359 + rkey: rkey.to_string(), 456 360 cid: record_cid, 457 361 prev: prev_record_cid, 458 362 }); 459 363 } 460 364 WriteOp::Delete { collection, rkey } => { 461 - let collection_nsid = match collection.parse::<Nsid>() { 462 - Ok(n) => n, 463 - Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection", "message": "Invalid collection NSID"}))).into_response(), 464 - }; 465 - let key = format!("{}/{}", collection_nsid, rkey); 365 + let key = format!("{}/{}", collection, rkey); 466 366 modified_keys.push(key.clone()); 467 367 let prev_record_cid = mst.get(&key).await.ok().flatten(); 468 368 mst = match mst.delete(&key).await { 469 369 Ok(m) => m, 470 - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to delete from MST"}))).into_response(), 370 + Err(_) => { 371 + return ApiError::InternalError(Some("Failed to delete from MST".into())) 372 + .into_response() 373 + } 471 374 }; 472 375 results.push(WriteResult::DeleteResult {}); 473 376 ops.push(RecordOp::Delete { 474 - collection: collection.clone(), 475 - rkey: rkey.clone(), 377 + collection: collection.to_string(), 378 + rkey: rkey.to_string(), 476 379 prev: prev_record_cid, 477 380 }); 478 381 } ··· 480 383 } 481 384 let new_mst_root = match mst.persist().await { 482 385 Ok(c) => c, 483 - Err(_) => { 484 - return ( 485 - StatusCode::INTERNAL_SERVER_ERROR, 486 - Json(json!({"error": "InternalError", "message": "Failed to persist MST"})), 487 - ) 488 - .into_response(); 489 - } 386 + Err(_) => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 490 387 }; 491 388 let mut relevant_blocks = std::collections::BTreeMap::new(); 492 389 for key in &modified_keys { ··· 495 392 .await 496 393 .is_err() 497 394 { 498 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response(); 395 + return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 396 + .into_response(); 499 397 } 500 398 if original_mst 501 399 .blocks_for_path(key, &mut relevant_blocks) 502 400 .await 503 401 .is_err() 504 402 { 505 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response(); 403 + return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 404 + .into_response(); 506 405 } 507 406 } 508 407 let mut written_cids = tracking_store.get_all_relevant_cids(); ··· 533 432 Ok(res) => res, 534 433 Err(e) => { 535 434 error!("Commit failed: {}", e); 536 - return ( 537 - StatusCode::INTERNAL_SERVER_ERROR, 538 - Json(json!({"error": "InternalError", "message": "Failed to commit changes"})), 539 - ) 540 - .into_response(); 435 + return ApiError::InternalError(Some("Failed to commit changes".into())).into_response(); 541 436 } 542 437 }; 543 438
+24 -62
src/api/repo/record/delete.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 2 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 3 4 use crate::delegation::{self, DelegationActionType}; 4 5 use crate::repo::tracking::TrackingBlockStore; 5 6 use crate::state::AppState; 7 + use crate::types::{AtIdentifier, Nsid, Rkey}; 6 8 use axum::{ 7 9 Json, 8 10 extract::State, ··· 10 12 response::{IntoResponse, Response}, 11 13 }; 12 14 use cid::Cid; 13 - use jacquard::types::string::Nsid; 14 15 use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 15 16 use serde::{Deserialize, Serialize}; 16 17 use serde_json::json; ··· 20 21 21 22 #[derive(Deserialize)] 22 23 pub struct DeleteRecordInput { 23 - pub repo: String, 24 - pub collection: String, 25 - pub rkey: String, 24 + pub repo: AtIdentifier, 25 + pub collection: Nsid, 26 + pub rkey: Rkey, 26 27 #[serde(rename = "swapRecord")] 27 28 pub swap_record: Option<String>, 28 29 #[serde(rename = "swapCommit")] ··· 68 69 .await 69 70 .unwrap_or(false) 70 71 { 71 - return ( 72 - StatusCode::BAD_REQUEST, 73 - Json(json!({ 74 - "error": "AccountMigrated", 75 - "message": "Account has been migrated. Repo operations are not allowed." 76 - })), 77 - ) 78 - .into_response(); 72 + return ApiError::AccountMigrated.into_response(); 79 73 } 80 74 81 75 let did = auth.did; ··· 86 80 if let Some(swap_commit) = &input.swap_commit 87 81 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 88 82 { 89 - return ( 90 - StatusCode::CONFLICT, 91 - Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"})), 92 - ) 93 - .into_response(); 83 + return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 94 84 } 95 85 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 96 86 let commit_bytes = match tracking_store.get(&current_root_cid).await { 97 87 Ok(Some(b)) => b, 98 - _ => { 99 - return ( 100 - StatusCode::INTERNAL_SERVER_ERROR, 101 - Json(json!({"error": "InternalError", "message": "Commit block not found"})), 102 - ) 103 - .into_response(); 104 - } 88 + _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 105 89 }; 106 90 let commit = match Commit::from_cbor(&commit_bytes) { 107 91 Ok(c) => c, 108 - _ => { 109 - return ( 110 - StatusCode::INTERNAL_SERVER_ERROR, 111 - Json(json!({"error": "InternalError", "message": "Failed to parse commit"})), 112 - ) 113 - .into_response(); 114 - } 92 + _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 115 93 }; 116 94 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 117 - let collection_nsid = match input.collection.parse::<Nsid>() { 118 - Ok(n) => n, 119 - Err(_) => { 120 - return ( 121 - StatusCode::BAD_REQUEST, 122 - Json(json!({"error": "InvalidCollection"})), 123 - ) 124 - .into_response(); 125 - } 126 - }; 127 - let key = format!("{}/{}", collection_nsid, input.rkey); 95 + let key = format!("{}/{}", input.collection, input.rkey); 128 96 if let Some(swap_record_str) = &input.swap_record { 129 97 let expected_cid = Cid::from_str(swap_record_str).ok(); 130 98 let actual_cid = mst.get(&key).await.ok().flatten(); 131 99 if expected_cid != actual_cid { 132 - return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Record has been modified or does not exist"}))).into_response(); 100 + return ApiError::InvalidSwap(Some("Record has been modified or does not exist".into())) 101 + .into_response(); 133 102 } 134 103 } 135 104 let prev_record_cid = mst.get(&key).await.ok().flatten(); ··· 140 109 Ok(m) => m, 141 110 Err(e) => { 142 111 error!("Failed to delete from MST: {:?}", e); 143 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to delete from MST: {:?}", e)}))).into_response(); 112 + return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e))) 113 + .into_response(); 144 114 } 145 115 }; 146 116 let new_mst_root = match new_mst.persist().await { 147 117 Ok(c) => c, 148 118 Err(e) => { 149 119 error!("Failed to persist MST: {:?}", e); 150 - return ( 151 - StatusCode::INTERNAL_SERVER_ERROR, 152 - Json(json!({"error": "InternalError", "message": "Failed to persist MST"})), 153 - ) 154 - .into_response(); 120 + return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 155 121 } 156 122 }; 157 - let collection_for_audit = input.collection.clone(); 158 - let rkey_for_audit = input.rkey.clone(); 123 + let collection_for_audit = input.collection.to_string(); 124 + let rkey_for_audit = input.rkey.to_string(); 159 125 let op = RecordOp::Delete { 160 - collection: input.collection, 161 - rkey: input.rkey, 126 + collection: input.collection.to_string(), 127 + rkey: rkey_for_audit.clone(), 162 128 prev: prev_record_cid, 163 129 }; 164 130 let mut relevant_blocks = std::collections::BTreeMap::new(); ··· 167 133 .await 168 134 .is_err() 169 135 { 170 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response(); 136 + return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 137 + .into_response(); 171 138 } 172 139 if mst 173 140 .blocks_for_path(&key, &mut relevant_blocks) 174 141 .await 175 142 .is_err() 176 143 { 177 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response(); 144 + return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 145 + .into_response(); 178 146 } 179 147 let mut written_cids = tracking_store.get_all_relevant_cids(); 180 148 for cid in relevant_blocks.keys() { ··· 202 170 .await 203 171 { 204 172 Ok(res) => res, 205 - Err(e) => { 206 - return ( 207 - StatusCode::INTERNAL_SERVER_ERROR, 208 - Json(json!({"error": "InternalError", "message": e})), 209 - ) 210 - .into_response(); 211 - } 173 + Err(e) => return ApiError::InternalError(Some(e)).into_response(), 212 174 }; 213 175 214 176 if let Some(ref controller) = controller_did {
+41 -84
src/api/repo/record/read.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 3 + use crate::types::{AtIdentifier, Nsid, Rkey}; 2 4 use axum::{ 3 5 Json, 4 6 extract::{Query, State}, 5 - http::{HeaderMap, StatusCode}, 7 + http::HeaderMap, 6 8 response::{IntoResponse, Response}, 7 9 }; 8 10 use base64::Engine; ··· 46 48 47 49 #[derive(Deserialize)] 48 50 pub struct GetRecordInput { 49 - pub repo: String, 50 - pub collection: String, 51 - pub rkey: String, 51 + pub repo: AtIdentifier, 52 + pub collection: Nsid, 53 + pub rkey: Rkey, 52 54 pub cid: Option<String>, 53 55 } 54 56 55 57 pub async fn get_record( 56 58 State(state): State<AppState>, 57 - headers: HeaderMap, 59 + _headers: HeaderMap, 58 60 Query(input): Query<GetRecordInput>, 59 61 ) -> Response { 60 62 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 61 - let user_id_opt = if input.repo.starts_with("did:") { 62 - sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo) 63 + let user_id_opt = if input.repo.is_did() { 64 + sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo.as_str()) 63 65 .fetch_optional(&state.db) 64 66 .await 65 67 .map(|opt| opt.map(|r| r.id)) 66 68 } else { 67 - let handle = if !input.repo.contains('.') { 68 - format!("{}.{}", input.repo, hostname) 69 + let repo_str = input.repo.as_str(); 70 + let handle = if !repo_str.contains('.') { 71 + format!("{}.{}", repo_str, hostname) 69 72 } else { 70 - input.repo.clone() 73 + repo_str.to_string() 71 74 }; 72 75 sqlx::query!("SELECT id FROM users WHERE handle = $1", handle) 73 76 .fetch_optional(&state.db) ··· 77 80 let user_id: uuid::Uuid = match user_id_opt { 78 81 Ok(Some(id)) => id, 79 82 Ok(None) => { 80 - return ( 81 - StatusCode::NOT_FOUND, 82 - Json(json!({"error": "RepoNotFound", "message": "Repo not found"})), 83 - ) 84 - .into_response(); 83 + return ApiError::RepoNotFound(Some("Repo not found".into())).into_response(); 85 84 } 86 85 Err(_) => { 87 - return ( 88 - StatusCode::INTERNAL_SERVER_ERROR, 89 - Json(json!({"error": "InternalError"})), 90 - ) 91 - .into_response(); 86 + return ApiError::InternalError(None).into_response(); 92 87 } 93 88 }; 94 89 let record_row = sqlx::query!( 95 90 "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3", 96 91 user_id, 97 - input.collection, 98 - input.rkey 92 + input.collection.as_str(), 93 + input.rkey.as_str() 99 94 ) 100 95 .fetch_optional(&state.db) 101 96 .await; 102 97 let record_cid_str: String = match record_row { 103 98 Ok(Some(row)) => row.record_cid, 104 99 _ => { 105 - return ( 106 - StatusCode::NOT_FOUND, 107 - Json(json!({"error": "RecordNotFound", "message": "Record not found"})), 108 - ) 109 - .into_response(); 100 + return ApiError::RecordNotFound.into_response(); 110 101 } 111 102 }; 112 103 if let Some(expected_cid) = &input.cid 113 104 && &record_cid_str != expected_cid 114 105 { 115 - return ( 116 - StatusCode::NOT_FOUND, 117 - Json(json!({"error": "RecordNotFound", "message": "Record CID mismatch"})), 118 - ) 119 - .into_response(); 106 + return ApiError::RecordNotFound.into_response(); 120 107 } 121 - let cid = match Cid::from_str(&record_cid_str) { 122 - Ok(c) => c, 123 - Err(_) => { 124 - return ( 125 - StatusCode::INTERNAL_SERVER_ERROR, 126 - Json(json!({"error": "InternalError", "message": "Invalid CID in DB"})), 127 - ) 128 - .into_response(); 129 - } 108 + let Ok(cid) = Cid::from_str(&record_cid_str) else { 109 + return ApiError::InternalError(Some("Invalid CID in DB".into())).into_response(); 130 110 }; 131 111 let block = match state.block_store.get(&cid).await { 132 112 Ok(Some(b)) => b, 133 113 _ => { 134 - return ( 135 - StatusCode::INTERNAL_SERVER_ERROR, 136 - Json(json!({"error": "InternalError", "message": "Record block not found"})), 137 - ) 138 - .into_response(); 114 + return ApiError::InternalError(Some("Record block not found".into())).into_response(); 139 115 } 140 116 }; 141 117 let ipld: Ipld = match serde_ipld_dagcbor::from_slice(&block) { 142 118 Ok(v) => v, 143 119 Err(e) => { 144 120 error!("Failed to deserialize record: {:?}", e); 145 - return ( 146 - StatusCode::INTERNAL_SERVER_ERROR, 147 - Json(json!({"error": "InternalError"})), 148 - ) 149 - .into_response(); 121 + return ApiError::InternalError(None).into_response(); 150 122 } 151 123 }; 152 124 let value = ipld_to_json(ipld); ··· 159 131 } 160 132 #[derive(Deserialize)] 161 133 pub struct ListRecordsInput { 162 - pub repo: String, 163 - pub collection: String, 134 + pub repo: AtIdentifier, 135 + pub collection: Nsid, 164 136 pub limit: Option<i32>, 165 137 pub cursor: Option<String>, 166 138 #[serde(rename = "rkeyStart")] 167 - pub rkey_start: Option<String>, 139 + pub rkey_start: Option<Rkey>, 168 140 #[serde(rename = "rkeyEnd")] 169 - pub rkey_end: Option<String>, 141 + pub rkey_end: Option<Rkey>, 170 142 pub reverse: Option<bool>, 171 143 } 172 144 #[derive(Serialize)] ··· 181 153 Query(input): Query<ListRecordsInput>, 182 154 ) -> Response { 183 155 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 184 - let user_id_opt = if input.repo.starts_with("did:") { 185 - sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo) 156 + let user_id_opt = if input.repo.is_did() { 157 + sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo.as_str()) 186 158 .fetch_optional(&state.db) 187 159 .await 188 160 .map(|opt| opt.map(|r| r.id)) 189 161 } else { 190 - let handle = if !input.repo.contains('.') { 191 - format!("{}.{}", input.repo, hostname) 162 + let repo_str = input.repo.as_str(); 163 + let handle = if !repo_str.contains('.') { 164 + format!("{}.{}", repo_str, hostname) 192 165 } else { 193 - input.repo.clone() 166 + repo_str.to_string() 194 167 }; 195 168 sqlx::query!("SELECT id FROM users WHERE handle = $1", handle) 196 169 .fetch_optional(&state.db) ··· 200 173 let user_id: uuid::Uuid = match user_id_opt { 201 174 Ok(Some(id)) => id, 202 175 Ok(None) => { 203 - return ( 204 - StatusCode::NOT_FOUND, 205 - Json(json!({"error": "RepoNotFound", "message": "Repo not found"})), 206 - ) 207 - .into_response(); 176 + return ApiError::RepoNotFound(Some("Repo not found".into())).into_response(); 208 177 } 209 178 Err(_) => { 210 - return ( 211 - StatusCode::INTERNAL_SERVER_ERROR, 212 - Json(json!({"error": "InternalError"})), 213 - ) 214 - .into_response(); 179 + return ApiError::InternalError(None).into_response(); 215 180 } 216 181 }; 217 182 let limit = input.limit.unwrap_or(50).clamp(1, 100); ··· 226 191 ); 227 192 sqlx::query_as(&query) 228 193 .bind(user_id) 229 - .bind(&input.collection) 194 + .bind(input.collection.as_str()) 230 195 .bind(cursor) 231 196 .bind(limit_i64) 232 197 .fetch_all(&state.db) ··· 255 220 ); 256 221 let mut query_builder = sqlx::query_as::<_, (String, String)>(&query) 257 222 .bind(user_id) 258 - .bind(&input.collection); 223 + .bind(input.collection.as_str()); 259 224 if let Some(start) = &input.rkey_start { 260 - query_builder = query_builder.bind(start); 225 + query_builder = query_builder.bind(start.as_str()); 261 226 } 262 227 if let Some(end) = &input.rkey_end { 263 - query_builder = query_builder.bind(end); 228 + query_builder = query_builder.bind(end.as_str()); 264 229 } 265 230 query_builder.bind(limit_i64).fetch_all(&state.db).await 266 231 }; ··· 268 233 Ok(r) => r, 269 234 Err(e) => { 270 235 error!("Error listing records: {:?}", e); 271 - return ( 272 - StatusCode::INTERNAL_SERVER_ERROR, 273 - Json(json!({"error": "InternalError"})), 274 - ) 275 - .into_response(); 236 + return ApiError::InternalError(None).into_response(); 276 237 } 277 238 }; 278 239 let last_rkey = rows.last().map(|(rkey, _)| rkey.clone()); ··· 288 249 Ok(b) => b, 289 250 Err(e) => { 290 251 error!("Error fetching blocks: {:?}", e); 291 - return ( 292 - StatusCode::INTERNAL_SERVER_ERROR, 293 - Json(json!({"error": "InternalError"})), 294 - ) 295 - .into_response(); 252 + return ApiError::InternalError(None).into_response(); 296 253 } 297 254 }; 298 255 let mut records = Vec::new();
+24 -63
src/api/repo/record/validation.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::validation::{RecordValidator, ValidationError, ValidationStatus}; 2 - use axum::{ 3 - Json, 4 - http::StatusCode, 5 - response::{IntoResponse, Response}, 6 - }; 7 - use serde_json::json; 3 + use axum::response::Response; 8 4 9 5 pub fn validate_record(record: &serde_json::Value, collection: &str) -> Result<(), Box<Response>> { 10 6 validate_record_with_rkey(record, collection, None) ··· 42 38 } 43 39 44 40 fn validation_error_to_box_response(e: ValidationError) -> Box<Response> { 45 - match e { 46 - ValidationError::MissingType => Box::new( 47 - ( 48 - StatusCode::BAD_REQUEST, 49 - Json(json!({"error": "InvalidRecord", "message": "Record must have a $type field"})), 50 - ) 51 - .into_response(), 52 - ), 53 - ValidationError::TypeMismatch { expected, actual } => Box::new( 54 - ( 55 - StatusCode::BAD_REQUEST, 56 - Json(json!({"error": "InvalidRecord", "message": format!("Record $type '{}' does not match collection '{}'", actual, expected)})), 41 + use axum::response::IntoResponse; 42 + let msg = match e { 43 + ValidationError::MissingType => "Record must have a $type field".to_string(), 44 + ValidationError::TypeMismatch { expected, actual } => { 45 + format!( 46 + "Record $type '{}' does not match collection '{}'", 47 + actual, expected 57 48 ) 58 - .into_response(), 59 - ), 60 - ValidationError::MissingField(field) => Box::new( 61 - ( 62 - StatusCode::BAD_REQUEST, 63 - Json(json!({"error": "InvalidRecord", "message": format!("Missing required field: {}", field)})), 64 - ) 65 - .into_response(), 66 - ), 67 - ValidationError::InvalidField { path, message } => Box::new( 68 - ( 69 - StatusCode::BAD_REQUEST, 70 - Json(json!({"error": "InvalidRecord", "message": format!("Invalid field '{}': {}", path, message)})), 71 - ) 72 - .into_response(), 73 - ), 74 - ValidationError::InvalidDatetime { path } => Box::new( 75 - ( 76 - StatusCode::BAD_REQUEST, 77 - Json(json!({"error": "InvalidRecord", "message": format!("Invalid datetime format at '{}'", path)})), 78 - ) 79 - .into_response(), 80 - ), 81 - ValidationError::BannedContent { path } => Box::new( 82 - ( 83 - StatusCode::BAD_REQUEST, 84 - Json(json!({"error": "InvalidRecord", "message": format!("Unacceptable slur in record at '{}'", path)})), 85 - ) 86 - .into_response(), 87 - ), 88 - ValidationError::UnknownType(type_name) => Box::new( 89 - ( 90 - StatusCode::BAD_REQUEST, 91 - Json(json!({"error": "InvalidRecord", "message": format!("Lexicon not found: lex:{}", type_name)})), 92 - ) 93 - .into_response(), 94 - ), 95 - e => Box::new( 96 - ( 97 - StatusCode::BAD_REQUEST, 98 - Json(json!({"error": "InvalidRecord", "message": e.to_string()})), 99 - ) 100 - .into_response(), 101 - ), 102 - } 49 + } 50 + ValidationError::MissingField(field) => format!("Missing required field: {}", field), 51 + ValidationError::InvalidField { path, message } => { 52 + format!("Invalid field '{}': {}", path, message) 53 + } 54 + ValidationError::InvalidDatetime { path } => { 55 + format!("Invalid datetime format at '{}'", path) 56 + } 57 + ValidationError::BannedContent { path } => { 58 + format!("Unacceptable slur in record at '{}'", path) 59 + } 60 + ValidationError::UnknownType(type_name) => format!("Lexicon not found: lex:{}", type_name), 61 + e => e.to_string(), 62 + }; 63 + Box::new(ApiError::InvalidRecord(msg).into_response()) 103 64 }
+72 -239
src/api/repo/record/write.rs
··· 1 1 use super::validation::validate_record_with_status; 2 + use crate::api::error::ApiError; 2 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 3 4 use crate::delegation::{self, DelegationActionType}; 4 5 use crate::repo::tracking::TrackingBlockStore; 5 6 use crate::state::AppState; 6 - use crate::validation::ValidationStatus; 7 + use crate::types::{AtIdentifier, AtUri, Did, Nsid, Rkey}; 7 8 use axum::{ 8 9 Json, 9 10 extract::State, ··· 11 12 response::{IntoResponse, Response}, 12 13 }; 13 14 use cid::Cid; 14 - use jacquard::types::{ 15 - integer::LimitedU32, 16 - string::{Nsid, Tid}, 17 - }; 18 15 use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 19 16 use serde::{Deserialize, Serialize}; 20 17 use serde_json::json; ··· 52 49 } 53 50 54 51 pub struct RepoWriteAuth { 55 - pub did: String, 52 + pub did: Did, 56 53 pub user_id: Uuid, 57 54 pub current_root_cid: Cid, 58 55 pub is_oauth: bool, 59 56 pub scope: Option<String>, 60 - pub controller_did: Option<String>, 57 + pub controller_did: Option<Did>, 61 58 } 62 59 63 60 pub async fn prepare_repo_write( ··· 70 67 let extracted = crate::auth::extract_auth_token_from_header( 71 68 headers.get("Authorization").and_then(|h| h.to_str().ok()), 72 69 ) 73 - .ok_or_else(|| { 74 - ( 75 - StatusCode::UNAUTHORIZED, 76 - Json(json!({"error": "AuthenticationRequired"})), 77 - ) 78 - .into_response() 79 - })?; 70 + .ok_or_else(|| ApiError::AuthenticationRequired.into_response())?; 80 71 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 81 72 let auth_user = crate::auth::validate_token_with_dpop( 82 73 &state.db, ··· 90 81 .await 91 82 .map_err(|e| { 92 83 tracing::warn!(error = ?e, is_dpop = extracted.is_dpop, "Token validation failed in prepare_repo_write"); 93 - let mut response = ( 94 - StatusCode::UNAUTHORIZED, 95 - Json(json!({"error": e.to_string()})), 96 - ) 97 - .into_response(); 84 + let mut response = ApiError::from(e).into_response(); 98 85 if matches!(e, crate::auth::TokenValidationError::TokenExpired) { 99 86 let scheme = if extracted.is_dpop { "DPoP" } else { "Bearer" }; 100 87 let www_auth = format!( ··· 113 100 response 114 101 })?; 115 102 if repo_did != auth_user.did { 116 - return Err(( 117 - StatusCode::FORBIDDEN, 118 - Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"})), 119 - ) 120 - .into_response()); 103 + return Err( 104 + ApiError::InvalidRepo("Repo does not match authenticated user".into()).into_response(), 105 + ); 121 106 } 122 107 if crate::util::is_account_migrated(&state.db, &auth_user.did) 123 108 .await 124 109 .unwrap_or(false) 125 110 { 126 - return Err(( 127 - StatusCode::FORBIDDEN, 128 - Json(json!({ 129 - "error": "AccountMigrated", 130 - "message": "Account has been migrated to another PDS. Repo operations are not allowed." 131 - })), 132 - ) 133 - .into_response()); 111 + return Err(ApiError::AccountMigrated.into_response()); 134 112 } 135 113 let is_verified = has_verified_comms_channel(&state.db, &auth_user.did) 136 114 .await ··· 139 117 .await 140 118 .unwrap_or(false); 141 119 if !is_verified && !is_delegated { 142 - return Err(( 143 - StatusCode::FORBIDDEN, 144 - Json(json!({ 145 - "error": "AccountNotVerified", 146 - "message": "You must verify at least one notification channel (email, Discord, Telegram, or Signal) before creating records" 147 - })), 148 - ) 149 - .into_response()); 120 + return Err(ApiError::AccountNotVerified.into_response()); 150 121 } 151 - let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 122 + let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", &auth_user.did) 152 123 .fetch_optional(&state.db) 153 124 .await 154 125 .map_err(|e| { 155 126 error!("DB error fetching user: {}", e); 156 - ( 157 - StatusCode::INTERNAL_SERVER_ERROR, 158 - Json(json!({"error": "InternalError"})), 159 - ) 160 - .into_response() 127 + ApiError::InternalError(None).into_response() 161 128 })? 162 - .ok_or_else(|| { 163 - ( 164 - StatusCode::INTERNAL_SERVER_ERROR, 165 - Json(json!({"error": "InternalError", "message": "User not found"})), 166 - ) 167 - .into_response() 168 - })?; 129 + .ok_or_else(|| ApiError::InternalError(Some("User not found".into())).into_response())?; 169 130 let root_cid_str: String = sqlx::query_scalar!( 170 131 "SELECT repo_root_cid FROM repos WHERE user_id = $1", 171 132 user_id ··· 174 135 .await 175 136 .map_err(|e| { 176 137 error!("DB error fetching repo root: {}", e); 177 - ( 178 - StatusCode::INTERNAL_SERVER_ERROR, 179 - Json(json!({"error": "InternalError"})), 180 - ) 181 - .into_response() 138 + ApiError::InternalError(None).into_response() 182 139 })? 183 - .ok_or_else(|| { 184 - ( 185 - StatusCode::INTERNAL_SERVER_ERROR, 186 - Json(json!({"error": "InternalError", "message": "Repo root not found"})), 187 - ) 188 - .into_response() 189 - })?; 190 - let current_root_cid = Cid::from_str(&root_cid_str).map_err(|_| { 191 - ( 192 - StatusCode::INTERNAL_SERVER_ERROR, 193 - Json(json!({"error": "InternalError", "message": "Invalid repo root CID"})), 194 - ) 195 - .into_response() 196 - })?; 140 + .ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())).into_response())?; 141 + let current_root_cid = Cid::from_str(&root_cid_str) 142 + .map_err(|_| ApiError::InternalError(Some("Invalid repo root CID".into())).into_response())?; 197 143 Ok(RepoWriteAuth { 198 - did: auth_user.did, 144 + did: auth_user.did.clone(), 199 145 user_id, 200 146 current_root_cid, 201 147 is_oauth: auth_user.is_oauth, 202 148 scope: auth_user.scope, 203 - controller_did: auth_user.controller_did, 149 + controller_did: auth_user.controller_did.clone(), 204 150 }) 205 151 } 206 152 #[derive(Deserialize)] 207 153 #[allow(dead_code)] 208 154 pub struct CreateRecordInput { 209 - pub repo: String, 210 - pub collection: String, 211 - pub rkey: Option<String>, 155 + pub repo: AtIdentifier, 156 + pub collection: Nsid, 157 + pub rkey: Option<Rkey>, 212 158 pub validate: Option<bool>, 213 159 pub record: serde_json::Value, 214 160 #[serde(rename = "swapCommit")] ··· 224 170 #[derive(Serialize)] 225 171 #[serde(rename_all = "camelCase")] 226 172 pub struct CreateRecordOutput { 227 - pub uri: String, 173 + pub uri: AtUri, 228 174 pub cid: String, 229 175 pub commit: CommitInfo, 230 176 #[serde(skip_serializing_if = "Option::is_none")] ··· 266 212 if let Some(swap_commit) = &input.swap_commit 267 213 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 268 214 { 269 - return ( 270 - StatusCode::CONFLICT, 271 - Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"})), 272 - ) 273 - .into_response(); 215 + return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 274 216 } 275 217 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 276 218 let commit_bytes = match tracking_store.get(&current_root_cid).await { 277 219 Ok(Some(b)) => b, 278 - _ => { 279 - return ( 280 - StatusCode::INTERNAL_SERVER_ERROR, 281 - Json(json!({"error": "InternalError", "message": "Commit block not found"})), 282 - ) 283 - .into_response(); 284 - } 220 + _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 285 221 }; 286 222 let commit = match Commit::from_cbor(&commit_bytes) { 287 223 Ok(c) => c, 288 - _ => { 289 - return ( 290 - StatusCode::INTERNAL_SERVER_ERROR, 291 - Json(json!({"error": "InternalError", "message": "Failed to parse commit"})), 292 - ) 293 - .into_response(); 294 - } 224 + _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 295 225 }; 296 226 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 297 - let collection_nsid = match input.collection.parse::<Nsid>() { 298 - Ok(n) => n, 299 - Err(_) => { 300 - return ( 301 - StatusCode::BAD_REQUEST, 302 - Json(json!({"error": "InvalidCollection"})), 303 - ) 304 - .into_response(); 305 - } 306 - }; 307 227 let validation_status = if input.validate == Some(false) { 308 228 None 309 229 } else { ··· 311 231 match validate_record_with_status( 312 232 &input.record, 313 233 &input.collection, 314 - input.rkey.as_deref(), 234 + input.rkey.as_ref().map(|r| r.as_str()), 315 235 require_lexicon, 316 236 ) { 317 237 Ok(status) => Some(status), 318 238 Err(err_response) => return *err_response, 319 239 } 320 240 }; 321 - let rkey = input 322 - .rkey 323 - .unwrap_or_else(|| Tid::now(LimitedU32::MIN).to_string()); 241 + let rkey = input.rkey.unwrap_or_else(Rkey::generate); 324 242 let record_ipld = crate::util::json_to_ipld(&input.record); 325 243 let mut record_bytes = Vec::new(); 326 244 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 327 - return ( 328 - StatusCode::BAD_REQUEST, 329 - Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})), 330 - ) 331 - .into_response(); 245 + return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 332 246 } 333 247 let record_cid = match tracking_store.put(&record_bytes).await { 334 248 Ok(c) => c, 335 249 _ => { 336 - return ( 337 - StatusCode::INTERNAL_SERVER_ERROR, 338 - Json(json!({"error": "InternalError", "message": "Failed to save record block"})), 339 - ) 340 - .into_response(); 250 + return ApiError::InternalError(Some("Failed to save record block".into())).into_response() 341 251 } 342 252 }; 343 - let key = format!("{}/{}", collection_nsid, rkey); 253 + let key = format!("{}/{}", input.collection, rkey); 344 254 let new_mst = match mst.add(&key, record_cid).await { 345 255 Ok(m) => m, 346 - _ => { 347 - return ( 348 - StatusCode::INTERNAL_SERVER_ERROR, 349 - Json(json!({"error": "InternalError", "message": "Failed to add to MST"})), 350 - ) 351 - .into_response(); 352 - } 256 + _ => return ApiError::InternalError(Some("Failed to add to MST".into())).into_response(), 353 257 }; 354 258 let new_mst_root = match new_mst.persist().await { 355 259 Ok(c) => c, 356 - _ => { 357 - return ( 358 - StatusCode::INTERNAL_SERVER_ERROR, 359 - Json(json!({"error": "InternalError", "message": "Failed to persist MST"})), 360 - ) 361 - .into_response(); 362 - } 260 + _ => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 363 261 }; 364 262 let op = RecordOp::Create { 365 - collection: input.collection.clone(), 366 - rkey: rkey.clone(), 263 + collection: input.collection.to_string(), 264 + rkey: rkey.to_string(), 367 265 cid: record_cid, 368 266 }; 369 267 let mut relevant_blocks = std::collections::BTreeMap::new(); ··· 372 270 .await 373 271 .is_err() 374 272 { 375 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response(); 273 + return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 274 + .into_response(); 376 275 } 377 276 if mst 378 277 .blocks_for_path(&key, &mut relevant_blocks) 379 278 .await 380 279 .is_err() 381 280 { 382 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response(); 281 + return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 282 + .into_response(); 383 283 } 384 284 relevant_blocks.insert(record_cid, bytes::Bytes::from(record_bytes)); 385 285 let mut written_cids = tracking_store.get_all_relevant_cids(); ··· 409 309 .await 410 310 { 411 311 Ok(res) => res, 412 - Err(e) => { 413 - return ( 414 - StatusCode::INTERNAL_SERVER_ERROR, 415 - Json(json!({"error": "InternalError", "message": e})), 416 - ) 417 - .into_response(); 418 - } 312 + Err(e) => return ApiError::InternalError(Some(e)).into_response(), 419 313 }; 420 314 421 315 if let Some(ref controller) = controller_did { ··· 439 333 ( 440 334 StatusCode::OK, 441 335 Json(CreateRecordOutput { 442 - uri: format!("at://{}/{}/{}", did, input.collection, rkey), 336 + uri: AtUri::from_parts(&did, &input.collection, &rkey), 443 337 cid: record_cid.to_string(), 444 338 commit: CommitInfo { 445 339 cid: commit_result.commit_cid.to_string(), 446 340 rev: commit_result.rev, 447 341 }, 448 - validation_status: validation_status.map(|s| match s { 449 - ValidationStatus::Valid => "valid".to_string(), 450 - ValidationStatus::Unknown => "unknown".to_string(), 451 - ValidationStatus::Invalid => "invalid".to_string(), 452 - }), 342 + validation_status: validation_status.map(|s| s.to_string()), 453 343 }), 454 344 ) 455 345 .into_response() ··· 457 347 #[derive(Deserialize)] 458 348 #[allow(dead_code)] 459 349 pub struct PutRecordInput { 460 - pub repo: String, 461 - pub collection: String, 462 - pub rkey: String, 350 + pub repo: AtIdentifier, 351 + pub collection: Nsid, 352 + pub rkey: Rkey, 463 353 pub validate: Option<bool>, 464 354 pub record: serde_json::Value, 465 355 #[serde(rename = "swapCommit")] ··· 470 360 #[derive(Serialize)] 471 361 #[serde(rename_all = "camelCase")] 472 362 pub struct PutRecordOutput { 473 - pub uri: String, 363 + pub uri: AtUri, 474 364 pub cid: String, 475 365 #[serde(skip_serializing_if = "Option::is_none")] 476 366 pub commit: Option<CommitInfo>, ··· 521 411 if let Some(swap_commit) = &input.swap_commit 522 412 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 523 413 { 524 - return ( 525 - StatusCode::CONFLICT, 526 - Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"})), 527 - ) 528 - .into_response(); 414 + return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 529 415 } 530 416 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 531 417 let commit_bytes = match tracking_store.get(&current_root_cid).await { 532 418 Ok(Some(b)) => b, 533 - _ => { 534 - return ( 535 - StatusCode::INTERNAL_SERVER_ERROR, 536 - Json(json!({"error": "InternalError", "message": "Commit block not found"})), 537 - ) 538 - .into_response(); 539 - } 419 + _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 540 420 }; 541 421 let commit = match Commit::from_cbor(&commit_bytes) { 542 422 Ok(c) => c, 543 - _ => { 544 - return ( 545 - StatusCode::INTERNAL_SERVER_ERROR, 546 - Json(json!({"error": "InternalError", "message": "Failed to parse commit"})), 547 - ) 548 - .into_response(); 549 - } 423 + _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 550 424 }; 551 425 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 552 - let collection_nsid = match input.collection.parse::<Nsid>() { 553 - Ok(n) => n, 554 - Err(_) => { 555 - return ( 556 - StatusCode::BAD_REQUEST, 557 - Json(json!({"error": "InvalidCollection"})), 558 - ) 559 - .into_response(); 560 - } 561 - }; 562 - let key = format!("{}/{}", collection_nsid, input.rkey); 426 + let key = format!("{}/{}", input.collection, input.rkey); 563 427 let validation_status = if input.validate == Some(false) { 564 428 None 565 429 } else { ··· 567 431 match validate_record_with_status( 568 432 &input.record, 569 433 &input.collection, 570 - Some(&input.rkey), 434 + Some(input.rkey.as_str()), 571 435 require_lexicon, 572 436 ) { 573 437 Ok(status) => Some(status), ··· 578 442 let expected_cid = Cid::from_str(swap_record_str).ok(); 579 443 let actual_cid = mst.get(&key).await.ok().flatten(); 580 444 if expected_cid != actual_cid { 581 - return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Record has been modified or does not exist"}))).into_response(); 445 + return ApiError::InvalidSwap(Some("Record has been modified or does not exist".into())) 446 + .into_response(); 582 447 } 583 448 } 584 449 let existing_cid = mst.get(&key).await.ok().flatten(); 585 450 let record_ipld = crate::util::json_to_ipld(&input.record); 586 451 let mut record_bytes = Vec::new(); 587 452 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 588 - return ( 589 - StatusCode::BAD_REQUEST, 590 - Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})), 591 - ) 592 - .into_response(); 453 + return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 593 454 } 594 455 let record_cid = match tracking_store.put(&record_bytes).await { 595 456 Ok(c) => c, 596 457 _ => { 597 - return ( 598 - StatusCode::INTERNAL_SERVER_ERROR, 599 - Json(json!({"error": "InternalError", "message": "Failed to save record block"})), 600 - ) 601 - .into_response(); 458 + return ApiError::InternalError(Some("Failed to save record block".into())).into_response() 602 459 } 603 460 }; 604 461 if existing_cid == Some(record_cid) { 605 462 return ( 606 463 StatusCode::OK, 607 464 Json(PutRecordOutput { 608 - uri: format!("at://{}/{}/{}", did, input.collection, input.rkey), 465 + uri: AtUri::from_parts(&did, &input.collection, &input.rkey), 609 466 cid: record_cid.to_string(), 610 467 commit: None, 611 - validation_status: validation_status.map(|s| match s { 612 - ValidationStatus::Valid => "valid".to_string(), 613 - ValidationStatus::Unknown => "unknown".to_string(), 614 - ValidationStatus::Invalid => "invalid".to_string(), 615 - }), 468 + validation_status: validation_status.map(|s| s.to_string()), 616 469 }), 617 470 ) 618 471 .into_response(); ··· 621 474 match mst.update(&key, record_cid).await { 622 475 Ok(m) => m, 623 476 Err(_) => { 624 - return ( 625 - StatusCode::INTERNAL_SERVER_ERROR, 626 - Json(json!({"error": "InternalError", "message": "Failed to update MST"})), 627 - ) 628 - .into_response(); 477 + return ApiError::InternalError(Some("Failed to update MST".into())).into_response() 629 478 } 630 479 } 631 480 } else { 632 481 match mst.add(&key, record_cid).await { 633 482 Ok(m) => m, 634 483 Err(_) => { 635 - return ( 636 - StatusCode::INTERNAL_SERVER_ERROR, 637 - Json(json!({"error": "InternalError", "message": "Failed to add to MST"})), 638 - ) 639 - .into_response(); 484 + return ApiError::InternalError(Some("Failed to add to MST".into())).into_response() 640 485 } 641 486 } 642 487 }; 643 488 let new_mst_root = match new_mst.persist().await { 644 489 Ok(c) => c, 645 490 Err(_) => { 646 - return ( 647 - StatusCode::INTERNAL_SERVER_ERROR, 648 - Json(json!({"error": "InternalError", "message": "Failed to persist MST"})), 649 - ) 650 - .into_response(); 491 + return ApiError::InternalError(Some("Failed to persist MST".into())).into_response() 651 492 } 652 493 }; 653 494 let op = if existing_cid.is_some() { 654 495 RecordOp::Update { 655 - collection: input.collection.clone(), 656 - rkey: input.rkey.clone(), 496 + collection: input.collection.to_string(), 497 + rkey: input.rkey.to_string(), 657 498 cid: record_cid, 658 499 prev: existing_cid, 659 500 } 660 501 } else { 661 502 RecordOp::Create { 662 - collection: input.collection.clone(), 663 - rkey: input.rkey.clone(), 503 + collection: input.collection.to_string(), 504 + rkey: input.rkey.to_string(), 664 505 cid: record_cid, 665 506 } 666 507 }; ··· 670 511 .await 671 512 .is_err() 672 513 { 673 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response(); 514 + return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 515 + .into_response(); 674 516 } 675 517 if mst 676 518 .blocks_for_path(&key, &mut relevant_blocks) 677 519 .await 678 520 .is_err() 679 521 { 680 - return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response(); 522 + return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 523 + .into_response(); 681 524 } 682 525 relevant_blocks.insert(record_cid, bytes::Bytes::from(record_bytes)); 683 526 let mut written_cids = tracking_store.get_all_relevant_cids(); ··· 708 551 .await 709 552 { 710 553 Ok(res) => res, 711 - Err(e) => { 712 - return ( 713 - StatusCode::INTERNAL_SERVER_ERROR, 714 - Json(json!({"error": "InternalError", "message": e})), 715 - ) 716 - .into_response(); 717 - } 554 + Err(e) => return ApiError::InternalError(Some(e)).into_response(), 718 555 }; 719 556 720 557 if let Some(ref controller) = controller_did { ··· 738 575 ( 739 576 StatusCode::OK, 740 577 Json(PutRecordOutput { 741 - uri: format!("at://{}/{}/{}", did, input.collection, input.rkey), 578 + uri: AtUri::from_parts(&did, &input.collection, &input.rkey), 742 579 cid: record_cid.to_string(), 743 580 commit: Some(CommitInfo { 744 581 cid: commit_result.commit_cid.to_string(), 745 582 rev: commit_result.rev, 746 583 }), 747 - validation_status: validation_status.map(|s| match s { 748 - ValidationStatus::Valid => "valid".to_string(), 749 - ValidationStatus::Unknown => "unknown".to_string(), 750 - ValidationStatus::Invalid => "invalid".to_string(), 751 - }), 584 + validation_status: validation_status.map(|s| s.to_string()), 752 585 }), 753 586 ) 754 587 .into_response()
+114
src/api/responses.rs
··· 1 + use crate::types::Did; 2 + use axum::{Json, response::IntoResponse}; 3 + use serde::Serialize; 4 + 5 + #[derive(Debug, Serialize)] 6 + pub struct EmptyResponse {} 7 + 8 + impl EmptyResponse { 9 + pub fn ok() -> impl IntoResponse { 10 + Json(Self {}) 11 + } 12 + } 13 + 14 + #[derive(Debug, Serialize)] 15 + pub struct SuccessResponse { 16 + pub success: bool, 17 + } 18 + 19 + impl SuccessResponse { 20 + pub fn ok() -> impl IntoResponse { 21 + Json(Self { success: true }) 22 + } 23 + } 24 + 25 + #[derive(Debug, Serialize)] 26 + pub struct DidResponse { 27 + pub did: Did, 28 + } 29 + 30 + impl DidResponse { 31 + pub fn new(did: impl Into<Did>) -> impl IntoResponse { 32 + Json(Self { did: did.into() }) 33 + } 34 + } 35 + 36 + #[derive(Debug, Serialize)] 37 + #[serde(rename_all = "camelCase")] 38 + pub struct TokenRequiredResponse { 39 + pub token_required: bool, 40 + } 41 + 42 + impl TokenRequiredResponse { 43 + pub fn new(required: bool) -> impl IntoResponse { 44 + Json(Self { token_required: required }) 45 + } 46 + } 47 + 48 + #[derive(Debug, Serialize)] 49 + #[serde(rename_all = "camelCase")] 50 + pub struct HasPasswordResponse { 51 + pub has_password: bool, 52 + } 53 + 54 + impl HasPasswordResponse { 55 + pub fn new(has_password: bool) -> impl IntoResponse { 56 + Json(Self { has_password }) 57 + } 58 + } 59 + 60 + #[derive(Debug, Serialize)] 61 + pub struct VerifiedResponse { 62 + pub verified: bool, 63 + } 64 + 65 + impl VerifiedResponse { 66 + pub fn new(verified: bool) -> impl IntoResponse { 67 + Json(Self { verified }) 68 + } 69 + } 70 + 71 + #[derive(Debug, Serialize)] 72 + pub struct EnabledResponse { 73 + pub enabled: bool, 74 + } 75 + 76 + impl EnabledResponse { 77 + pub fn new(enabled: bool) -> impl IntoResponse { 78 + Json(Self { enabled }) 79 + } 80 + } 81 + 82 + #[derive(Debug, Serialize)] 83 + pub struct StatusResponse { 84 + pub status: String, 85 + } 86 + 87 + impl StatusResponse { 88 + pub fn new(status: impl Into<String>) -> impl IntoResponse { 89 + Json(Self { status: status.into() }) 90 + } 91 + } 92 + 93 + #[derive(Debug, Serialize)] 94 + #[serde(rename_all = "camelCase")] 95 + pub struct DidDocumentResponse { 96 + pub did_document: serde_json::Value, 97 + } 98 + 99 + impl DidDocumentResponse { 100 + pub fn new(did_document: serde_json::Value) -> impl IntoResponse { 101 + Json(Self { did_document }) 102 + } 103 + } 104 + 105 + #[derive(Debug, Serialize)] 106 + pub struct OptionsResponse<T: Serialize> { 107 + pub options: T, 108 + } 109 + 110 + impl<T: Serialize> OptionsResponse<T> { 111 + pub fn new(options: T) -> Json<Self> { 112 + Json(Self { options }) 113 + } 114 + }
+67 -185
src/api/server/account_status.rs
··· 1 - use crate::api::ApiError; 1 + use crate::api::error::ApiError; 2 + use crate::api::EmptyResponse; 2 3 use crate::cache::Cache; 3 4 use crate::plc::PlcClient; 4 5 use crate::state::AppState; 6 + use crate::types::PlainPassword; 5 7 use axum::{ 6 8 Json, 7 9 extract::State, ··· 15 17 use jacquard_repo::storage::BlockStore; 16 18 use k256::ecdsa::SigningKey; 17 19 use serde::{Deserialize, Serialize}; 18 - use serde_json::json; 19 20 use std::str::FromStr; 20 21 use std::sync::Arc; 21 22 use tracing::{error, info, warn}; ··· 64 65 Ok(user) => user.did, 65 66 Err(e) => return ApiError::from(e).into_response(), 66 67 }; 67 - let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 68 + let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str()) 68 69 .fetch_optional(&state.db) 69 70 .await 70 71 { 71 72 Ok(Some(id)) => id, 72 73 _ => { 73 - return ( 74 - StatusCode::INTERNAL_SERVER_ERROR, 75 - Json(json!({"error": "InternalError"})), 76 - ) 77 - .into_response(); 74 + return ApiError::InternalError(None).into_response(); 78 75 } 79 76 }; 80 - let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did) 77 + let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did.as_str()) 81 78 .fetch_optional(&state.db) 82 79 .await; 83 80 let deactivated_at = match user_status { ··· 142 139 .await 143 140 .unwrap_or(Some(0)) 144 141 .unwrap_or(0); 145 - let valid_did = is_valid_did_for_service(&state.db, &state.cache, &did).await; 142 + let valid_did = is_valid_did_for_service(&state.db, state.cache.clone(), did.as_str()).await; 146 143 ( 147 144 StatusCode::OK, 148 145 Json(CheckAccountStatusOutput { ··· 160 157 .into_response() 161 158 } 162 159 163 - async fn is_valid_did_for_service(db: &sqlx::PgPool, cache: &Arc<dyn Cache>, did: &str) -> bool { 160 + async fn is_valid_did_for_service(db: &sqlx::PgPool, cache: Arc<dyn Cache>, did: &str) -> bool { 164 161 assert_valid_did_document_for_service(db, cache, did, false) 165 162 .await 166 163 .is_ok() ··· 168 165 169 166 async fn assert_valid_did_document_for_service( 170 167 db: &sqlx::PgPool, 171 - cache: &Arc<dyn Cache>, 168 + cache: Arc<dyn Cache>, 172 169 did: &str, 173 170 with_retry: bool, 174 - ) -> Result<(), (StatusCode, Json<serde_json::Value>)> { 171 + ) -> Result<(), ApiError> { 175 172 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 176 173 let expected_endpoint = format!("https://{}", hostname); 177 174 ··· 228 225 } 229 226 } 230 227 231 - let doc_data = match doc_data { 232 - Some(d) => d, 233 - None => { 234 - return Err(( 235 - StatusCode::BAD_REQUEST, 236 - Json(json!({ 237 - "error": "InvalidRequest", 238 - "message": last_error.unwrap_or_else(|| "DID document validation failed".to_string()) 239 - })), 240 - )); 241 - } 228 + let Some(doc_data) = doc_data else { 229 + return Err(ApiError::InvalidRequest( 230 + last_error.unwrap_or_else(|| "DID document validation failed".to_string()), 231 + )); 242 232 }; 243 233 244 234 let server_rotation_key = std::env::var("PLC_ROTATION_KEY").ok(); ··· 249 239 .map(|arr| arr.iter().filter_map(|k| k.as_str()).collect::<Vec<_>>()) 250 240 .unwrap_or_default(); 251 241 if !rotation_keys.contains(&expected_rotation_key.as_str()) { 252 - return Err(( 253 - StatusCode::BAD_REQUEST, 254 - Json(json!({ 255 - "error": "InvalidRequest", 256 - "message": "Server rotation key not included in PLC DID data" 257 - })), 242 + return Err(ApiError::InvalidRequest( 243 + "Server rotation key not included in PLC DID data".into(), 258 244 )); 259 245 } 260 246 } ··· 272 258 .await 273 259 .map_err(|e| { 274 260 error!("Failed to fetch user key: {:?}", e); 275 - ( 276 - StatusCode::INTERNAL_SERVER_ERROR, 277 - Json(json!({"error": "InternalError"})), 278 - ) 261 + ApiError::InternalError(None) 279 262 })?; 280 263 281 264 if let Some(row) = user_row { 282 265 let key_bytes = crate::config::decrypt_key(&row.key_bytes, row.encryption_version) 283 266 .map_err(|e| { 284 267 error!("Failed to decrypt user key: {}", e); 285 - ( 286 - StatusCode::INTERNAL_SERVER_ERROR, 287 - Json(json!({"error": "InternalError"})), 288 - ) 268 + ApiError::InternalError(None) 289 269 })?; 290 270 let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 291 271 error!("Failed to create signing key: {:?}", e); 292 - ( 293 - StatusCode::INTERNAL_SERVER_ERROR, 294 - Json(json!({"error": "InternalError"})), 295 - ) 272 + ApiError::InternalError(None) 296 273 })?; 297 274 let expected_did_key = crate::plc::signing_key_to_did_key(&signing_key); 298 275 ··· 301 278 "DID {} has signing key {:?}, expected {}", 302 279 did, doc_signing_key, expected_did_key 303 280 ); 304 - return Err(( 305 - StatusCode::BAD_REQUEST, 306 - Json(json!({ 307 - "error": "InvalidRequest", 308 - "message": "DID document verification method does not match expected signing key" 309 - })), 281 + return Err(ApiError::InvalidRequest( 282 + "DID document verification method does not match expected signing key".into(), 310 283 )); 311 284 } 312 285 } ··· 333 306 }; 334 307 let resp = client.get(&url).send().await.map_err(|e| { 335 308 warn!("Failed to fetch did:web document for {}: {:?}", did, e); 336 - ( 337 - StatusCode::BAD_REQUEST, 338 - Json(json!({ 339 - "error": "InvalidRequest", 340 - "message": format!("Could not resolve DID document: {}", e) 341 - })), 342 - ) 309 + ApiError::InvalidRequest(format!("Could not resolve DID document: {}", e)) 343 310 })?; 344 311 let doc: serde_json::Value = resp.json().await.map_err(|e| { 345 312 warn!("Failed to parse did:web document for {}: {:?}", did, e); 346 - ( 347 - StatusCode::BAD_REQUEST, 348 - Json(json!({ 349 - "error": "InvalidRequest", 350 - "message": format!("Could not parse DID document: {}", e) 351 - })), 352 - ) 313 + ApiError::InvalidRequest(format!("Could not parse DID document: {}", e)) 353 314 })?; 354 315 355 316 let pds_endpoint = doc ··· 370 331 "DID {} has endpoint {:?}, expected {}", 371 332 did, pds_endpoint, expected_endpoint 372 333 ); 373 - return Err(( 374 - StatusCode::BAD_REQUEST, 375 - Json(json!({ 376 - "error": "InvalidRequest", 377 - "message": "DID document atproto_pds service endpoint does not match PDS public url" 378 - })), 334 + return Err(ApiError::InvalidRequest( 335 + "DID document atproto_pds service endpoint does not match PDS public url".into(), 379 336 )); 380 337 } 381 338 } ··· 441 398 did 442 399 ); 443 400 let did_validation_start = std::time::Instant::now(); 444 - if let Err((status, json)) = 445 - assert_valid_did_document_for_service(&state.db, &state.cache, &did, true).await 401 + if let Err(e) = 402 + assert_valid_did_document_for_service(&state.db, state.cache.clone(), did.as_str(), true).await 446 403 { 447 404 info!( 448 405 "[MIGRATION] activateAccount: DID document validation FAILED for {} (took {:?})", 449 406 did, 450 407 did_validation_start.elapsed() 451 408 ); 452 - return (status, json).into_response(); 409 + return e.into_response(); 453 410 } 454 411 info!( 455 412 "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", ··· 457 414 did_validation_start.elapsed() 458 415 ); 459 416 460 - let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did) 417 + let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did.as_str()) 461 418 .fetch_optional(&state.db) 462 419 .await 463 420 .ok() ··· 466 423 "[MIGRATION] activateAccount: Activating account did={} handle={:?}", 467 424 did, handle 468 425 ); 469 - let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did) 426 + let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did.as_str()) 470 427 .execute(&state.db) 471 428 .await; 472 429 match result { ··· 483 440 did 484 441 ); 485 442 if let Err(e) = 486 - crate::api::repo::record::sequence_account_event(&state, &did, true, None).await 443 + crate::api::repo::record::sequence_account_event(&state, did.as_str(), true, None).await 487 444 { 488 445 warn!( 489 446 "[MIGRATION] activateAccount: Failed to sequence account activation event: {}", ··· 497 454 did, handle 498 455 ); 499 456 if let Err(e) = 500 - crate::api::repo::record::sequence_identity_event(&state, &did, handle.as_deref()) 457 + crate::api::repo::record::sequence_identity_event(&state, did.as_str(), handle.as_deref()) 501 458 .await 502 459 { 503 460 warn!( ··· 509 466 } 510 467 let repo_root = sqlx::query_scalar!( 511 468 "SELECT r.repo_root_cid FROM repos r JOIN users u ON r.user_id = u.id WHERE u.did = $1", 512 - did 469 + did.as_str() 513 470 ) 514 471 .fetch_optional(&state.db) 515 472 .await ··· 531 488 }; 532 489 if let Err(e) = crate::api::repo::record::sequence_sync_event( 533 490 &state, 534 - &did, 491 + did.as_str(), 535 492 &root_cid, 536 493 rev.as_deref(), 537 494 ) ··· 551 508 ); 552 509 } 553 510 info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); 554 - (StatusCode::OK, Json(json!({}))).into_response() 511 + EmptyResponse::ok().into_response() 555 512 } 556 513 Err(e) => { 557 514 error!( 558 515 "[MIGRATION] activateAccount: DB error activating account: {:?}", 559 516 e 560 517 ); 561 - ( 562 - StatusCode::INTERNAL_SERVER_ERROR, 563 - Json(json!({"error": "InternalError"})), 564 - ) 565 - .into_response() 518 + ApiError::InternalError(None).into_response() 566 519 } 567 520 } 568 521 } ··· 621 574 622 575 let did = auth_user.did; 623 576 624 - let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did) 577 + let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did.as_str()) 625 578 .fetch_optional(&state.db) 626 579 .await 627 580 .ok() ··· 629 582 630 583 let result = sqlx::query!( 631 584 "UPDATE users SET deactivated_at = NOW(), delete_after = $2 WHERE did = $1", 632 - did, 585 + did.as_str(), 633 586 delete_after 634 587 ) 635 588 .execute(&state.db) ··· 642 595 } 643 596 if let Err(e) = crate::api::repo::record::sequence_account_event( 644 597 &state, 645 - &did, 598 + did.as_str(), 646 599 false, 647 600 Some("deactivated"), 648 601 ) ··· 650 603 { 651 604 warn!("Failed to sequence account deactivated event: {}", e); 652 605 } 653 - (StatusCode::OK, Json(json!({}))).into_response() 606 + EmptyResponse::ok().into_response() 654 607 } 655 608 Err(e) => { 656 609 error!("DB error deactivating account: {:?}", e); 657 - ( 658 - StatusCode::INTERNAL_SERVER_ERROR, 659 - Json(json!({"error": "InternalError"})), 660 - ) 661 - .into_response() 610 + ApiError::InternalError(None).into_response() 662 611 } 663 612 } 664 613 } ··· 694 643 }; 695 644 let did = validated.did.clone(); 696 645 697 - if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &did).await { 698 - return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &did).await; 646 + if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, did.as_str()).await { 647 + return crate::api::server::reauth::legacy_mfa_required_response(&state.db, did.as_str()).await; 699 648 } 700 649 701 - let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 650 + let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str()) 702 651 .fetch_optional(&state.db) 703 652 .await 704 653 { 705 654 Ok(Some(id)) => id, 706 655 _ => { 707 - return ( 708 - StatusCode::INTERNAL_SERVER_ERROR, 709 - Json(json!({"error": "InternalError"})), 710 - ) 711 - .into_response(); 656 + return ApiError::InternalError(None).into_response(); 712 657 } 713 658 }; 714 659 let confirmation_token = Uuid::new_v4().to_string(); ··· 716 661 let insert = sqlx::query!( 717 662 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)", 718 663 confirmation_token, 719 - did, 664 + did.as_str(), 720 665 expires_at 721 666 ) 722 667 .execute(&state.db) 723 668 .await; 724 669 if let Err(e) = insert { 725 670 error!("DB error creating deletion token: {:?}", e); 726 - return ( 727 - StatusCode::INTERNAL_SERVER_ERROR, 728 - Json(json!({"error": "InternalError"})), 729 - ) 730 - .into_response(); 671 + return ApiError::InternalError(None).into_response(); 731 672 } 732 673 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 733 674 if let Err(e) = ··· 737 678 warn!("Failed to enqueue account deletion notification: {:?}", e); 738 679 } 739 680 info!("Account deletion requested for user {}", did); 740 - (StatusCode::OK, Json(json!({}))).into_response() 681 + EmptyResponse::ok().into_response() 741 682 } 742 683 743 684 #[derive(Deserialize)] 744 685 pub struct DeleteAccountInput { 745 - pub did: String, 746 - pub password: String, 686 + pub did: crate::types::Did, 687 + pub password: PlainPassword, 747 688 pub token: String, 748 689 } 749 690 ··· 751 692 State(state): State<AppState>, 752 693 Json(input): Json<DeleteAccountInput>, 753 694 ) -> Response { 754 - let did = input.did.trim(); 695 + let did = &input.did; 755 696 let password = &input.password; 756 697 let token = input.token.trim(); 757 - if did.is_empty() { 758 - return ( 759 - StatusCode::BAD_REQUEST, 760 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 761 - ) 762 - .into_response(); 763 - } 764 698 if password.is_empty() { 765 - return ( 766 - StatusCode::BAD_REQUEST, 767 - Json(json!({"error": "InvalidRequest", "message": "password is required"})), 768 - ) 769 - .into_response(); 699 + return ApiError::InvalidRequest("password is required".into()).into_response(); 770 700 } 771 701 const OLD_PASSWORD_MAX_LENGTH: usize = 512; 772 702 if password.len() > OLD_PASSWORD_MAX_LENGTH { 773 - return ( 774 - StatusCode::BAD_REQUEST, 775 - Json(json!({"error": "InvalidRequest", "message": "Invalid password length."})), 776 - ) 777 - .into_response(); 703 + return ApiError::InvalidRequest("Invalid password length".into()).into_response(); 778 704 } 779 705 if token.is_empty() { 780 - return ( 781 - StatusCode::BAD_REQUEST, 782 - Json(json!({"error": "InvalidToken", "message": "token is required"})), 783 - ) 784 - .into_response(); 706 + return ApiError::InvalidToken(Some("token is required".into())).into_response(); 785 707 } 786 708 let user = sqlx::query!( 787 709 "SELECT id, password_hash, handle FROM users WHERE did = $1", 788 - did 710 + did.as_str() 789 711 ) 790 712 .fetch_optional(&state.db) 791 713 .await; 792 714 let (user_id, password_hash, handle) = match user { 793 715 Ok(Some(row)) => (row.id, row.password_hash, row.handle), 794 716 Ok(None) => { 795 - return ( 796 - StatusCode::BAD_REQUEST, 797 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 798 - ) 799 - .into_response(); 717 + return ApiError::InvalidRequest("account not found".into()).into_response(); 800 718 } 801 719 Err(e) => { 802 720 error!("DB error in delete_account: {:?}", e); 803 - return ( 804 - StatusCode::INTERNAL_SERVER_ERROR, 805 - Json(json!({"error": "InternalError"})), 806 - ) 807 - .into_response(); 721 + return ApiError::InternalError(None).into_response(); 808 722 } 809 723 }; 810 724 let password_valid = if password_hash ··· 826 740 .any(|row| verify(password, &row.password_hash).unwrap_or(false)) 827 741 }; 828 742 if !password_valid { 829 - return ( 830 - StatusCode::UNAUTHORIZED, 831 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid password"})), 832 - ) 833 - .into_response(); 743 + return ApiError::AuthenticationFailed(Some("Invalid password".into())).into_response(); 834 744 } 835 745 let deletion_request = sqlx::query!( 836 746 "SELECT did, expires_at FROM account_deletion_requests WHERE token = $1", ··· 841 751 let (token_did, expires_at) = match deletion_request { 842 752 Ok(Some(row)) => (row.did, row.expires_at), 843 753 Ok(None) => { 844 - return ( 845 - StatusCode::BAD_REQUEST, 846 - Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 847 - ) 848 - .into_response(); 754 + return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response(); 849 755 } 850 756 Err(e) => { 851 757 error!("DB error fetching deletion token: {:?}", e); 852 - return ( 853 - StatusCode::INTERNAL_SERVER_ERROR, 854 - Json(json!({"error": "InternalError"})), 855 - ) 856 - .into_response(); 758 + return ApiError::InternalError(None).into_response(); 857 759 } 858 760 }; 859 - if token_did != did { 860 - return ( 861 - StatusCode::BAD_REQUEST, 862 - Json(json!({"error": "InvalidToken", "message": "Token does not match account"})), 863 - ) 864 - .into_response(); 761 + if token_did != did.as_str() { 762 + return ApiError::InvalidToken(Some("Token does not match account".into())).into_response(); 865 763 } 866 764 if Utc::now() > expires_at { 867 765 let _ = sqlx::query!( ··· 870 768 ) 871 769 .execute(&state.db) 872 770 .await; 873 - return ( 874 - StatusCode::BAD_REQUEST, 875 - Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 876 - ) 877 - .into_response(); 771 + return ApiError::ExpiredToken(None).into_response(); 878 772 } 879 773 let mut tx = match state.db.begin().await { 880 774 Ok(tx) => tx, 881 775 Err(e) => { 882 776 error!("Failed to begin transaction: {:?}", e); 883 - return ( 884 - StatusCode::INTERNAL_SERVER_ERROR, 885 - Json(json!({"error": "InternalError"})), 886 - ) 887 - .into_response(); 777 + return ApiError::InternalError(None).into_response(); 888 778 } 889 779 }; 890 780 let deletion_result: Result<(), sqlx::Error> = async { ··· 919 809 Ok(()) => { 920 810 if let Err(e) = tx.commit().await { 921 811 error!("Failed to commit account deletion transaction: {:?}", e); 922 - return ( 923 - StatusCode::INTERNAL_SERVER_ERROR, 924 - Json(json!({"error": "InternalError"})), 925 - ) 926 - .into_response(); 812 + return ApiError::InternalError(None).into_response(); 927 813 } 928 814 let account_seq = crate::api::repo::record::sequence_account_event( 929 815 &state, ··· 957 843 } 958 844 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 959 845 info!("Account {} deleted successfully", did); 960 - (StatusCode::OK, Json(json!({}))).into_response() 846 + EmptyResponse::ok().into_response() 961 847 } 962 848 Err(e) => { 963 849 error!("DB error deleting account, rolling back: {:?}", e); 964 - ( 965 - StatusCode::INTERNAL_SERVER_ERROR, 966 - Json(json!({"error": "InternalError"})), 967 - ) 968 - .into_response() 850 + ApiError::InternalError(None).into_response() 969 851 } 970 852 } 971 853 }
+15 -21
src/api/server/app_password.rs
··· 1 - use crate::api::ApiError; 1 + use crate::api::error::ApiError; 2 + use crate::api::EmptyResponse; 2 3 use crate::auth::BearerAuth; 3 4 use crate::delegation::{self, DelegationActionType}; 4 5 use crate::state::{AppState, RateLimitKind}; ··· 60 61 } 61 62 Err(e) => { 62 63 error!("DB error listing app passwords: {:?}", e); 63 - ApiError::InternalError.into_response() 64 + ApiError::InternalError(None).into_response() 64 65 } 65 66 } 66 67 } ··· 95 96 .await 96 97 { 97 98 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 98 - return ( 99 - axum::http::StatusCode::TOO_MANY_REQUESTS, 100 - Json(json!({ 101 - "error": "RateLimitExceeded", 102 - "message": "Too many requests. Please try again later." 103 - })), 104 - ) 105 - .into_response(); 99 + return ApiError::RateLimitExceeded(None).into_response(); 106 100 } 107 101 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 108 102 Ok(id) => id, ··· 134 128 let intersected = delegation::intersect_scopes(requested, &granted_scopes); 135 129 136 130 if intersected.is_empty() && !granted_scopes.is_empty() { 137 - return ApiError::InsufficientScope.into_response(); 131 + return ApiError::InsufficientScope(None).into_response(); 138 132 } 139 133 140 134 let scope_result = if intersected.is_empty() { ··· 167 161 Ok(Ok(h)) => h, 168 162 Ok(Err(e)) => { 169 163 error!("Failed to hash password: {:?}", e); 170 - return ApiError::InternalError.into_response(); 164 + return ApiError::InternalError(None).into_response(); 171 165 } 172 166 Err(e) => { 173 167 error!("Failed to spawn blocking task: {:?}", e); 174 - return ApiError::InternalError.into_response(); 168 + return ApiError::InternalError(None).into_response(); 175 169 } 176 170 }; 177 171 let privileged = input.privileged.unwrap_or(false); ··· 184 178 created_at, 185 179 privileged, 186 180 final_scopes, 187 - controller_did 181 + controller_did.as_deref() 188 182 ) 189 183 .execute(&state.db) 190 184 .await ··· 218 212 } 219 213 Err(e) => { 220 214 error!("DB error creating app password: {:?}", e); 221 - ApiError::InternalError.into_response() 215 + ApiError::InternalError(None).into_response() 222 216 } 223 217 } 224 218 } ··· 243 237 } 244 238 let sessions_to_invalidate = sqlx::query_scalar!( 245 239 "SELECT access_jti FROM session_tokens WHERE did = $1 AND app_password_name = $2", 246 - auth_user.did, 240 + &auth_user.did, 247 241 name 248 242 ) 249 243 .fetch_all(&state.db) ··· 251 245 .unwrap_or_default(); 252 246 if let Err(e) = sqlx::query!( 253 247 "DELETE FROM session_tokens WHERE did = $1 AND app_password_name = $2", 254 - auth_user.did, 248 + &auth_user.did, 255 249 name 256 250 ) 257 251 .execute(&state.db) 258 252 .await 259 253 { 260 254 error!("DB error revoking sessions for app password: {:?}", e); 261 - return ApiError::InternalError.into_response(); 255 + return ApiError::InternalError(None).into_response(); 262 256 } 263 257 for jti in &sessions_to_invalidate { 264 - let cache_key = format!("auth:session:{}:{}", auth_user.did, jti); 258 + let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); 265 259 let _ = state.cache.delete(&cache_key).await; 266 260 } 267 261 if let Err(e) = sqlx::query!( ··· 273 267 .await 274 268 { 275 269 error!("DB error revoking app password: {:?}", e); 276 - return ApiError::InternalError.into_response(); 270 + return ApiError::InternalError(None).into_response(); 277 271 } 278 - Json(json!({})).into_response() 272 + EmptyResponse::ok().into_response() 279 273 }
+48 -195
src/api/server/email.rs
··· 1 - use crate::api::ApiError; 1 + use crate::api::error::ApiError; 2 + use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 2 3 use crate::auth::BearerAuth; 3 4 use crate::state::{AppState, RateLimitKind}; 4 5 use axum::{ 5 6 Json, 6 7 extract::State, 7 - http::StatusCode, 8 8 response::{IntoResponse, Response}, 9 9 }; 10 10 use serde::Deserialize; ··· 22 22 .await 23 23 { 24 24 warn!(ip = %client_ip, "Email update rate limit exceeded"); 25 - return ( 26 - StatusCode::TOO_MANY_REQUESTS, 27 - Json(json!({ 28 - "error": "RateLimitExceeded", 29 - "message": "Too many requests. Please try again later." 30 - })), 31 - ) 32 - .into_response(); 25 + return ApiError::RateLimitExceeded(None).into_response(); 33 26 } 34 27 35 28 if let Err(e) = crate::auth::scope_check::check_account_scope( ··· 41 34 return e; 42 35 } 43 36 44 - let did = auth.0.did.clone(); 37 + let did = auth.0.did.to_string(); 45 38 let user = match sqlx::query!( 46 39 "SELECT id, handle, email, email_verified FROM users WHERE did = $1", 47 40 did ··· 51 44 { 52 45 Ok(Some(row)) => row, 53 46 Ok(None) => { 54 - return ( 55 - StatusCode::BAD_REQUEST, 56 - Json(json!({"error": "InvalidRequest", "message": "account not found"})), 57 - ) 58 - .into_response(); 47 + return ApiError::AccountNotFound.into_response(); 59 48 } 60 49 Err(e) => { 61 50 error!("DB error: {:?}", e); 62 - return ( 63 - StatusCode::INTERNAL_SERVER_ERROR, 64 - Json(json!({"error": "InternalError"})), 65 - ) 66 - .into_response(); 51 + return ApiError::InternalError(None).into_response(); 67 52 } 68 53 }; 69 54 70 - let current_email: String = match user.email { 71 - Some(e) => e, 72 - None => { 73 - return ( 74 - StatusCode::BAD_REQUEST, 75 - Json(json!({"error": "InvalidRequest", "message": "account does not have an email address"})), 76 - ) 77 - .into_response(); 78 - } 55 + let Some(current_email) = user.email else { 56 + return ApiError::InvalidRequest("account does not have an email address".into()) 57 + .into_response(); 79 58 }; 80 59 81 60 let token_required = user.email_verified; ··· 98 77 } 99 78 100 79 info!("Email update requested for user {}", user.id); 101 - ( 102 - StatusCode::OK, 103 - Json(json!({ "tokenRequired": token_required })), 104 - ) 105 - .into_response() 80 + TokenRequiredResponse::new(token_required).into_response() 106 81 } 107 82 108 83 #[derive(Deserialize)] ··· 124 99 .await 125 100 { 126 101 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 127 - return ( 128 - StatusCode::TOO_MANY_REQUESTS, 129 - Json(json!({ 130 - "error": "RateLimitExceeded", 131 - "message": "Too many requests. Please try again later." 132 - })), 133 - ) 134 - .into_response(); 102 + return ApiError::RateLimitExceeded(None).into_response(); 135 103 } 136 104 137 105 if let Err(e) = crate::auth::scope_check::check_account_scope( ··· 143 111 return e; 144 112 } 145 113 146 - let did = auth.0.did; 114 + let did = auth.0.did.to_string(); 147 115 let user = match sqlx::query!( 148 116 "SELECT id, email, email_verified FROM users WHERE did = $1", 149 117 did ··· 153 121 { 154 122 Ok(Some(row)) => row, 155 123 Ok(None) => { 156 - return ( 157 - StatusCode::BAD_REQUEST, 158 - Json(json!({"error": "AccountNotFound", "message": "user not found"})), 159 - ) 160 - .into_response(); 124 + return ApiError::AccountNotFound.into_response(); 161 125 } 162 126 Err(e) => { 163 127 error!("DB error: {:?}", e); 164 - return ( 165 - StatusCode::INTERNAL_SERVER_ERROR, 166 - Json(json!({"error": "InternalError"})), 167 - ) 168 - .into_response(); 128 + return ApiError::InternalError(None).into_response(); 169 129 } 170 130 }; 171 131 172 - let current_email = match &user.email { 173 - Some(e) => e.to_lowercase(), 174 - None => { 175 - return ( 176 - StatusCode::BAD_REQUEST, 177 - Json(json!({"error": "InvalidEmail", "message": "account does not have an email address"})), 178 - ) 179 - .into_response(); 180 - } 132 + let Some(ref email) = user.email else { 133 + return ApiError::InvalidEmail.into_response(); 181 134 }; 135 + let current_email = email.to_lowercase(); 182 136 183 137 let provided_email = input.email.trim().to_lowercase(); 184 138 if provided_email != current_email { 185 - return ( 186 - StatusCode::BAD_REQUEST, 187 - Json(json!({"error": "InvalidEmail", "message": "invalid email"})), 188 - ) 189 - .into_response(); 139 + return ApiError::InvalidEmail.into_response(); 190 140 } 191 141 192 142 if user.email_verified { 193 - return (StatusCode::OK, Json(json!({}))).into_response(); 143 + return EmptyResponse::ok().into_response(); 194 144 } 195 145 196 146 let confirmation_code = ··· 205 155 match verified { 206 156 Ok(token_data) => { 207 157 if token_data.did != did { 208 - return ( 209 - StatusCode::BAD_REQUEST, 210 - Json( 211 - json!({"error": "InvalidToken", "message": "Token does not match account"}), 212 - ), 213 - ) 214 - .into_response(); 158 + return ApiError::InvalidToken(None).into_response(); 215 159 } 216 160 } 217 161 Err(crate::auth::verification_token::VerifyError::Expired) => { 218 - return ( 219 - StatusCode::BAD_REQUEST, 220 - Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 221 - ) 222 - .into_response(); 162 + return ApiError::ExpiredToken(None).into_response(); 223 163 } 224 164 Err(_) => { 225 - return ( 226 - StatusCode::BAD_REQUEST, 227 - Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 228 - ) 229 - .into_response(); 165 + return ApiError::InvalidToken(None).into_response(); 230 166 } 231 167 } 232 168 ··· 239 175 240 176 if let Err(e) = update { 241 177 error!("DB error confirming email: {:?}", e); 242 - return ( 243 - StatusCode::INTERNAL_SERVER_ERROR, 244 - Json(json!({"error": "InternalError"})), 245 - ) 246 - .into_response(); 178 + return ApiError::InternalError(None).into_response(); 247 179 } 248 180 249 181 info!("Email confirmed for user {}", user.id); 250 - (StatusCode::OK, Json(json!({}))).into_response() 182 + EmptyResponse::ok().into_response() 251 183 } 252 184 253 185 #[derive(Deserialize)] ··· 264 196 headers: axum::http::HeaderMap, 265 197 Json(input): Json<UpdateEmailInput>, 266 198 ) -> Response { 267 - let bearer_token = match crate::auth::extract_bearer_token_from_header( 199 + let Some(bearer_token) = crate::auth::extract_bearer_token_from_header( 268 200 headers.get("Authorization").and_then(|h| h.to_str().ok()), 269 - ) { 270 - Some(t) => t, 271 - None => { 272 - return ( 273 - StatusCode::UNAUTHORIZED, 274 - Json(json!({"error": "AuthenticationRequired"})), 275 - ) 276 - .into_response(); 277 - } 201 + ) else { 202 + return ApiError::AuthenticationRequired.into_response(); 278 203 }; 279 204 280 205 let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await; ··· 292 217 return e; 293 218 } 294 219 295 - let did = auth_user.did; 220 + let did = auth_user.did.to_string(); 296 221 let user = match sqlx::query!( 297 222 "SELECT id, email, email_verified FROM users WHERE did = $1", 298 223 did ··· 302 227 { 303 228 Ok(Some(row)) => row, 304 229 Ok(None) => { 305 - return ( 306 - StatusCode::BAD_REQUEST, 307 - Json(json!({"error": "InvalidRequest", "message": "account not found"})), 308 - ) 309 - .into_response(); 230 + return ApiError::AccountNotFound.into_response(); 310 231 } 311 232 Err(e) => { 312 233 error!("DB error: {:?}", e); 313 - return ( 314 - StatusCode::INTERNAL_SERVER_ERROR, 315 - Json(json!({"error": "InternalError"})), 316 - ) 317 - .into_response(); 234 + return ApiError::InternalError(None).into_response(); 318 235 } 319 236 }; 320 237 ··· 324 241 let new_email = input.email.trim().to_lowercase(); 325 242 326 243 if !crate::api::validation::is_valid_email(&new_email) { 327 - return ( 328 - StatusCode::BAD_REQUEST, 329 - Json(json!({ 330 - "error": "InvalidRequest", 331 - "message": "This email address is not supported, please use a different email." 332 - })), 244 + return ApiError::InvalidRequest( 245 + "This email address is not supported, please use a different email.".into(), 333 246 ) 334 - .into_response(); 247 + .into_response(); 335 248 } 336 249 337 250 if let Some(ref current) = current_email 338 251 && new_email == current.to_lowercase() 339 252 { 340 - return (StatusCode::OK, Json(json!({}))).into_response(); 253 + return EmptyResponse::ok().into_response(); 341 254 } 342 255 343 256 if email_verified { 344 - let confirmation_token = match &input.token { 345 - Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()), 346 - None => { 347 - return ( 348 - StatusCode::BAD_REQUEST, 349 - Json(json!({ 350 - "error": "TokenRequired", 351 - "message": "confirmation token required" 352 - })), 353 - ) 354 - .into_response(); 355 - } 257 + let Some(ref t) = input.token else { 258 + return ApiError::TokenRequired.into_response(); 356 259 }; 260 + let confirmation_token = crate::auth::verification_token::normalize_token_input(t.trim()); 357 261 358 262 let current_email_lower = current_email 359 263 .as_ref() ··· 369 273 match verified { 370 274 Ok(token_data) => { 371 275 if token_data.did != did { 372 - return ( 373 - StatusCode::BAD_REQUEST, 374 - Json( 375 - json!({"error": "InvalidToken", "message": "Token does not match account"}), 376 - ), 377 - ) 378 - .into_response(); 276 + return ApiError::InvalidToken(None).into_response(); 379 277 } 380 278 } 381 279 Err(crate::auth::verification_token::VerifyError::Expired) => { 382 - return ( 383 - StatusCode::BAD_REQUEST, 384 - Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 385 - ) 386 - .into_response(); 280 + return ApiError::ExpiredToken(None).into_response(); 387 281 } 388 282 Err(_) => { 389 - return ( 390 - StatusCode::BAD_REQUEST, 391 - Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 392 - ) 393 - .into_response(); 283 + return ApiError::InvalidToken(None).into_response(); 394 284 } 395 285 } 396 286 } ··· 404 294 .await; 405 295 406 296 if let Ok(Some(_)) = exists { 407 - return ( 408 - StatusCode::BAD_REQUEST, 409 - Json(json!({ 410 - "error": "InvalidRequest", 411 - "message": "This email address is already in use, please use a different email." 412 - })), 413 - ) 414 - .into_response(); 297 + return ApiError::InvalidRequest("Email is already in use".into()).into_response(); 415 298 } 416 299 417 300 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!( ··· 428 311 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation()) 429 312 .unwrap_or(false) 430 313 { 431 - return ( 432 - StatusCode::BAD_REQUEST, 433 - Json(json!({ 434 - "error": "InvalidRequest", 435 - "message": "This email address is already in use, please use a different email." 436 - })), 437 - ) 438 - .into_response(); 314 + return ApiError::EmailTaken.into_response(); 439 315 } 440 - return ( 441 - StatusCode::INTERNAL_SERVER_ERROR, 442 - Json(json!({"error": "InternalError"})), 443 - ) 444 - .into_response(); 316 + return ApiError::InternalError(None).into_response(); 445 317 } 446 318 447 319 let verification_token = ··· 474 346 } 475 347 476 348 info!("Email updated for user {}", user_id); 477 - (StatusCode::OK, Json(json!({}))).into_response() 349 + EmptyResponse::ok().into_response() 478 350 } 479 351 480 352 #[derive(Deserialize)] ··· 492 364 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 493 365 .await 494 366 { 495 - return ( 496 - StatusCode::TOO_MANY_REQUESTS, 497 - Json(json!({ 498 - "error": "RateLimitExceeded", 499 - "message": "Too many requests. Please try again later." 500 - })), 501 - ) 502 - .into_response(); 367 + return ApiError::RateLimitExceeded(None).into_response(); 503 368 } 504 369 505 370 let user = sqlx::query!( ··· 510 375 .await; 511 376 512 377 match user { 513 - Ok(Some(row)) => ( 514 - StatusCode::OK, 515 - Json(json!({ "verified": row.email_verified })), 516 - ) 517 - .into_response(), 518 - Ok(None) => ( 519 - StatusCode::NOT_FOUND, 520 - Json(json!({ "error": "AccountNotFound", "message": "Account not found" })), 521 - ) 522 - .into_response(), 378 + Ok(Some(row)) => VerifiedResponse::new(row.email_verified).into_response(), 379 + Ok(None) => ApiError::AccountNotFound.into_response(), 523 380 Err(e) => { 524 381 error!("DB error checking email verified: {:?}", e); 525 - ( 526 - StatusCode::INTERNAL_SERVER_ERROR, 527 - Json(json!({ "error": "InternalError" })), 528 - ) 529 - .into_response() 382 + ApiError::InternalError(None).into_response() 530 383 } 531 384 } 532 385 }
+9 -9
src/api/server/invite.rs
··· 53 53 return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 54 54 } 55 55 56 - let for_account = input.for_account.unwrap_or_else(|| auth_user.did.clone()); 56 + let for_account = input.for_account.unwrap_or_else(|| auth_user.did.to_string()); 57 57 let code = gen_invite_code(); 58 58 59 59 match sqlx::query!( ··· 69 69 Ok(result) => { 70 70 if result.rows_affected() == 0 { 71 71 error!("No admin user found to create invite code"); 72 - return ApiError::InternalError.into_response(); 72 + return ApiError::InternalError(None).into_response(); 73 73 } 74 74 Json(CreateInviteCodeOutput { code }).into_response() 75 75 } 76 76 Err(e) => { 77 77 error!("DB error creating invite code: {:?}", e); 78 - ApiError::InternalError.into_response() 78 + ApiError::InternalError(None).into_response() 79 79 } 80 80 } 81 81 } ··· 112 112 let for_accounts = input 113 113 .for_accounts 114 114 .filter(|v| !v.is_empty()) 115 - .unwrap_or_else(|| vec![auth_user.did.clone()]); 115 + .unwrap_or_else(|| vec![auth_user.did.to_string()]); 116 116 117 117 let admin_user_id = 118 118 match sqlx::query_scalar!("SELECT id FROM users WHERE is_admin = true LIMIT 1") ··· 122 122 Ok(Some(id)) => id, 123 123 Ok(None) => { 124 124 error!("No admin user found to create invite codes"); 125 - return ApiError::InternalError.into_response(); 125 + return ApiError::InternalError(None).into_response(); 126 126 } 127 127 Err(e) => { 128 128 error!("DB error looking up admin user: {:?}", e); 129 - return ApiError::InternalError.into_response(); 129 + return ApiError::InternalError(None).into_response(); 130 130 } 131 131 }; 132 132 ··· 147 147 .await 148 148 { 149 149 error!("DB error creating invite code: {:?}", e); 150 - return ApiError::InternalError.into_response(); 150 + return ApiError::InternalError(None).into_response(); 151 151 } 152 152 codes.push(code); 153 153 } ··· 213 213 WHERE ic.for_account = $1 214 214 ORDER BY ic.created_at DESC 215 215 "#, 216 - auth_user.did 216 + &auth_user.did 217 217 ) 218 218 .fetch_all(&state.db) 219 219 .await ··· 221 221 Ok(rows) => rows, 222 222 Err(e) => { 223 223 error!("DB error fetching invite codes: {:?}", e); 224 - return ApiError::InternalError.into_response(); 224 + return ApiError::InternalError(None).into_response(); 225 225 } 226 226 }; 227 227
+12 -20
src/api/server/migration.rs
··· 66 66 }; 67 67 68 68 if !auth_user.did.starts_with("did:web:") { 69 - return ( 70 - StatusCode::BAD_REQUEST, 71 - Json(json!({ 72 - "error": "InvalidRequest", 73 - "message": "DID document updates are only available for did:web accounts" 74 - })), 69 + return ApiError::InvalidRequest( 70 + "DID document updates are only available for did:web accounts".into(), 75 71 ) 76 - .into_response(); 72 + .into_response(); 77 73 } 78 74 79 75 let user = match sqlx::query!( 80 76 "SELECT id, handle, deactivated_at FROM users WHERE did = $1", 81 - auth_user.did 77 + &auth_user.did 82 78 ) 83 79 .fetch_optional(&state.db) 84 80 .await ··· 87 83 Ok(None) => return ApiError::AccountNotFound.into_response(), 88 84 Err(e) => { 89 85 tracing::error!("DB error getting user: {:?}", e); 90 - return ApiError::InternalError.into_response(); 86 + return ApiError::InternalError(None).into_response(); 91 87 } 92 88 }; 93 89 ··· 171 167 172 168 if let Err(e) = upsert_result { 173 169 tracing::error!("DB error upserting did_web_overrides: {:?}", e); 174 - return ApiError::InternalError.into_response(); 170 + return ApiError::InternalError(None).into_response(); 175 171 } 176 172 177 173 if let Some(ref endpoint) = input.service_endpoint { ··· 180 176 "UPDATE users SET migrated_to_pds = $1, migrated_at = $2 WHERE did = $3", 181 177 endpoint_clean, 182 178 now, 183 - auth_user.did 179 + &auth_user.did 184 180 ) 185 181 .execute(&state.db) 186 182 .await; 187 183 188 184 if let Err(e) = update_result { 189 185 tracing::error!("DB error updating service endpoint: {:?}", e); 190 - return ApiError::InternalError.into_response(); 186 + return ApiError::InternalError(None).into_response(); 191 187 } 192 188 } 193 189 194 190 let did_doc = build_did_document(&state.db, &auth_user.did).await; 195 191 196 - tracing::info!("Updated DID document for {}", auth_user.did); 192 + tracing::info!("Updated DID document for {}", &auth_user.did); 197 193 198 194 ( 199 195 StatusCode::OK, ··· 236 232 }; 237 233 238 234 if !auth_user.did.starts_with("did:web:") { 239 - return ( 240 - StatusCode::BAD_REQUEST, 241 - Json(json!({ 242 - "error": "InvalidRequest", 243 - "message": "This endpoint is only available for did:web accounts" 244 - })), 235 + return ApiError::InvalidRequest( 236 + "This endpoint is only available for did:web accounts".into(), 245 237 ) 246 - .into_response(); 238 + .into_response(); 247 239 } 248 240 249 241 let did_doc = build_did_document(&state.db, &auth_user.did).await;
+120 -397
src/api/server/passkey_account.rs
··· 1 + use crate::api::SuccessResponse; 2 + use crate::api::error::ApiError; 1 3 use axum::{ 2 4 Json, 3 5 extract::State, 4 - http::{HeaderMap, StatusCode}, 6 + http::HeaderMap, 5 7 response::{IntoResponse, Response}, 6 8 }; 7 9 use bcrypt::{DEFAULT_COST, hash}; ··· 18 20 use crate::api::repo::record::utils::create_signed_commit; 19 21 use crate::auth::{ServiceTokenVerifier, extract_bearer_token_from_header, is_service_token}; 20 22 use crate::state::{AppState, RateLimitKind}; 23 + use crate::types::{Did, Handle, PlainPassword}; 21 24 use crate::validation::validate_password; 22 25 23 26 fn extract_client_ip(headers: &HeaderMap) -> String { ··· 80 83 #[derive(Serialize)] 81 84 #[serde(rename_all = "camelCase")] 82 85 pub struct CreatePasskeyAccountResponse { 83 - pub did: String, 84 - pub handle: String, 86 + pub did: Did, 87 + pub handle: Handle, 85 88 pub setup_token: String, 86 89 pub setup_expires_at: chrono::DateTime<Utc>, 87 90 #[serde(skip_serializing_if = "Option::is_none")] ··· 99 102 .await 100 103 { 101 104 warn!(ip = %client_ip, "Account creation rate limit exceeded"); 102 - return ( 103 - StatusCode::TOO_MANY_REQUESTS, 104 - Json(json!({ 105 - "error": "RateLimitExceeded", 106 - "message": "Too many account creation attempts. Please try again later." 107 - })), 108 - ) 109 - .into_response(); 105 + return ApiError::RateLimitExceeded(Some("Too many account creation attempts. Please try again later.".into(),)) 106 + .into_response(); 110 107 } 111 108 112 109 let byod_auth = if let Some(token) = ··· 127 124 } 128 125 Err(e) => { 129 126 error!("Service token verification failed: {:?}", e); 130 - return ( 131 - StatusCode::UNAUTHORIZED, 132 - Json(json!({ 133 - "error": "AuthenticationFailed", 134 - "message": format!("Service token verification failed: {}", e) 135 - })), 136 - ) 137 - .into_response(); 127 + return ApiError::AuthenticationFailed(Some(format!( 128 + "Service token verification failed: {}", 129 + e 130 + ))) 131 + .into_response(); 138 132 } 139 133 } 140 134 } else { ··· 165 159 }; 166 160 match crate::api::validation::validate_short_handle(handle_to_validate) { 167 161 Ok(h) => format!("{}.{}", h, hostname), 168 - Err(e) => { 169 - return ( 170 - StatusCode::BAD_REQUEST, 171 - Json(json!({"error": "InvalidHandle", "message": e.to_string()})), 172 - ) 173 - .into_response(); 162 + Err(_) => { 163 + return ApiError::InvalidHandle(None).into_response(); 174 164 } 175 165 } 176 166 } else { ··· 185 175 if let Some(ref email) = email 186 176 && !crate::api::validation::is_valid_email(email) 187 177 { 188 - return ( 189 - StatusCode::BAD_REQUEST, 190 - Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 191 - ) 192 - .into_response(); 178 + return ApiError::InvalidEmail.into_response(); 193 179 } 194 180 195 181 if let Some(ref code) = input.invite_code { ··· 204 190 .unwrap_or(Some(false)); 205 191 206 192 if valid != Some(true) { 207 - return ( 208 - StatusCode::BAD_REQUEST, 209 - Json(json!({"error": "InvalidInviteCode", "message": "Invalid or expired invite code"})), 210 - ) 211 - .into_response(); 193 + return ApiError::InvalidInviteCode.into_response(); 212 194 } 213 195 } else { 214 196 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 215 197 .map(|v| v == "true" || v == "1") 216 198 .unwrap_or(false); 217 199 if invite_required { 218 - return ( 219 - StatusCode::BAD_REQUEST, 220 - Json(json!({"error": "InviteCodeRequired", "message": "An invite code is required to create an account"})), 221 - ) 222 - .into_response(); 200 + return ApiError::InviteCodeRequired.into_response(); 223 201 } 224 202 } 225 203 ··· 227 205 let verification_recipient = match verification_channel { 228 206 "email" => match &email { 229 207 Some(e) if !e.is_empty() => e.clone(), 230 - _ => return ( 231 - StatusCode::BAD_REQUEST, 232 - Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})), 233 - ).into_response(), 208 + _ => return ApiError::MissingEmail.into_response(), 234 209 }, 235 210 "discord" => match &input.discord_id { 236 211 Some(id) if !id.trim().is_empty() => id.trim().to_string(), 237 - _ => return ( 238 - StatusCode::BAD_REQUEST, 239 - Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})), 240 - ).into_response(), 212 + _ => return ApiError::MissingDiscordId.into_response(), 241 213 }, 242 214 "telegram" => match &input.telegram_username { 243 215 Some(username) if !username.trim().is_empty() => username.trim().to_string(), 244 - _ => return ( 245 - StatusCode::BAD_REQUEST, 246 - Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})), 247 - ).into_response(), 216 + _ => return ApiError::MissingTelegramUsername.into_response(), 248 217 }, 249 218 "signal" => match &input.signal_number { 250 219 Some(number) if !number.trim().is_empty() => number.trim().to_string(), 251 - _ => return ( 252 - StatusCode::BAD_REQUEST, 253 - Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})), 254 - ).into_response(), 220 + _ => return ApiError::MissingSignalNumber.into_response(), 255 221 }, 256 - _ => return ( 257 - StatusCode::BAD_REQUEST, 258 - Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})), 259 - ).into_response(), 222 + _ => return ApiError::InvalidVerificationChannel.into_response(), 260 223 }; 261 224 262 225 use k256::ecdsa::SigningKey; ··· 283 246 match reserved { 284 247 Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 285 248 Ok(None) => { 286 - return ( 287 - StatusCode::BAD_REQUEST, 288 - Json(json!({ 289 - "error": "InvalidSigningKey", 290 - "message": "Signing key not found, already used, or expired" 291 - })), 292 - ) 293 - .into_response(); 249 + return ApiError::InvalidSigningKey.into_response(); 294 250 } 295 251 Err(e) => { 296 252 error!("Error looking up reserved signing key: {:?}", e); 297 - return ( 298 - StatusCode::INTERNAL_SERVER_ERROR, 299 - Json(json!({"error": "InternalError"})), 300 - ) 301 - .into_response(); 253 + return ApiError::InternalError(None).into_response(); 302 254 } 303 255 } 304 256 } else { ··· 310 262 Ok(k) => k, 311 263 Err(e) => { 312 264 error!("Error creating signing key: {:?}", e); 313 - return ( 314 - StatusCode::INTERNAL_SERVER_ERROR, 315 - Json(json!({"error": "InternalError"})), 316 - ) 317 - .into_response(); 265 + return ApiError::InternalError(None).into_response(); 318 266 } 319 267 }; 320 268 ··· 330 278 let d = match &input.did { 331 279 Some(d) if !d.trim().is_empty() => d.trim(), 332 280 _ => { 333 - return ( 334 - StatusCode::BAD_REQUEST, 335 - Json(json!({"error": "InvalidRequest", "message": "External did:web requires the 'did' field to be provided"})), 281 + return ApiError::InvalidRequest( 282 + "External did:web requires the 'did' field to be provided".into(), 336 283 ) 337 - .into_response(); 284 + .into_response(); 338 285 } 339 286 }; 340 287 if !d.starts_with("did:web:") { 341 - return ( 342 - StatusCode::BAD_REQUEST, 343 - Json( 344 - json!({"error": "InvalidDid", "message": "External DID must be a did:web"}), 345 - ), 346 - ) 288 + return ApiError::InvalidDid("External DID must be a did:web".into()) 347 289 .into_response(); 348 290 } 349 291 if is_byod_did_web { 350 292 if let Some(ref auth_did) = byod_auth 351 293 && d != auth_did 352 294 { 353 - return ( 354 - StatusCode::FORBIDDEN, 355 - Json(json!({ 356 - "error": "AuthorizationError", 357 - "message": format!("Service token issuer {} does not match DID {}", auth_did, d) 358 - })), 359 - ) 360 - .into_response(); 295 + return ApiError::AuthorizationError(format!( 296 + "Service token issuer {} does not match DID {}", 297 + auth_did, d 298 + )) 299 + .into_response(); 361 300 } 362 301 info!(did = %d, "Creating external did:web passkey account (BYOD key)"); 363 302 } else { ··· 369 308 ) 370 309 .await 371 310 { 372 - return ( 373 - StatusCode::BAD_REQUEST, 374 - Json(json!({"error": "InvalidDid", "message": e})), 375 - ) 376 - .into_response(); 311 + return ApiError::InvalidDid(e).into_response(); 377 312 } 378 313 info!(did = %d, "Creating external did:web passkey account (reserved key)"); 379 314 } ··· 384 319 if let Some(ref provided_did) = input.did { 385 320 if provided_did.starts_with("did:plc:") { 386 321 if provided_did != auth_did { 387 - return ( 388 - StatusCode::FORBIDDEN, 389 - Json(json!({ 390 - "error": "AuthorizationError", 391 - "message": format!("Service token issuer {} does not match DID {}", auth_did, provided_did) 392 - })), 393 - ) 394 - .into_response(); 322 + return ApiError::AuthorizationError(format!( 323 + "Service token issuer {} does not match DID {}", 324 + auth_did, provided_did 325 + )) 326 + .into_response(); 395 327 } 396 328 info!(did = %provided_did, "Creating BYOD did:plc passkey account (migration)"); 397 329 provided_did.clone() 398 330 } else { 399 - return ( 400 - StatusCode::BAD_REQUEST, 401 - Json(json!({ 402 - "error": "InvalidRequest", 403 - "message": "BYOD migration requires a did:plc or did:web DID" 404 - })), 331 + return ApiError::InvalidRequest( 332 + "BYOD migration requires a did:plc or did:web DID".into(), 405 333 ) 406 - .into_response(); 334 + .into_response(); 407 335 } 408 336 } else { 409 - return ( 410 - StatusCode::BAD_REQUEST, 411 - Json(json!({ 412 - "error": "InvalidRequest", 413 - "message": "BYOD migration requires the 'did' field" 414 - })), 337 + return ApiError::InvalidRequest( 338 + "BYOD migration requires the 'did' field".into(), 415 339 ) 416 - .into_response(); 340 + .into_response(); 417 341 } 418 342 } else { 419 343 let rotation_key = std::env::var("PLC_ROTATION_KEY") ··· 428 352 Ok(r) => r, 429 353 Err(e) => { 430 354 error!("Error creating PLC genesis operation: {:?}", e); 431 - return ( 432 - StatusCode::INTERNAL_SERVER_ERROR, 433 - Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 434 - ) 355 + return ApiError::InternalError(Some("Failed to create PLC operation".into())) 435 356 .into_response(); 436 357 } 437 358 }; ··· 442 363 .await 443 364 { 444 365 error!("Failed to submit PLC genesis operation: {:?}", e); 445 - return ( 446 - StatusCode::BAD_GATEWAY, 447 - Json(json!({ 448 - "error": "UpstreamError", 449 - "message": format!("Failed to register DID with PLC directory: {}", e) 450 - })), 451 - ) 452 - .into_response(); 366 + return ApiError::UpstreamErrorMsg(format!( 367 + "Failed to register DID with PLC directory: {}", 368 + e 369 + )) 370 + .into_response(); 453 371 } 454 372 genesis_result.did 455 373 } ··· 463 381 Ok(h) => h, 464 382 Err(e) => { 465 383 error!("Error hashing setup token: {:?}", e); 466 - return ( 467 - StatusCode::INTERNAL_SERVER_ERROR, 468 - Json(json!({"error": "InternalError"})), 469 - ) 470 - .into_response(); 384 + return ApiError::InternalError(None).into_response(); 471 385 } 472 386 }; 473 387 let setup_expires_at = Utc::now() + Duration::hours(1); ··· 476 390 Ok(tx) => tx, 477 391 Err(e) => { 478 392 error!("Error starting transaction: {:?}", e); 479 - return ( 480 - StatusCode::INTERNAL_SERVER_ERROR, 481 - Json(json!({"error": "InternalError"})), 482 - ) 483 - .into_response(); 393 + return ApiError::InternalError(None).into_response(); 484 394 } 485 395 }; 486 396 ··· 545 455 { 546 456 let constraint = db_err.constraint().unwrap_or(""); 547 457 if constraint.contains("handle") { 548 - return ( 549 - StatusCode::BAD_REQUEST, 550 - Json(json!({"error": "HandleNotAvailable", "message": "Handle already taken"})), 551 - ) 552 - .into_response(); 458 + return ApiError::HandleNotAvailable(None).into_response(); 553 459 } else if constraint.contains("email") { 554 - return ( 555 - StatusCode::BAD_REQUEST, 556 - Json( 557 - json!({"error": "InvalidEmail", "message": "Email already registered"}), 558 - ), 559 - ) 560 - .into_response(); 460 + return ApiError::EmailTaken.into_response(); 561 461 } 562 462 } 563 463 error!("Error inserting user: {:?}", e); 564 - return ( 565 - StatusCode::INTERNAL_SERVER_ERROR, 566 - Json(json!({"error": "InternalError"})), 567 - ) 568 - .into_response(); 464 + return ApiError::InternalError(None).into_response(); 569 465 } 570 466 }; 571 467 ··· 573 469 Ok(bytes) => bytes, 574 470 Err(e) => { 575 471 error!("Error encrypting signing key: {:?}", e); 576 - return ( 577 - StatusCode::INTERNAL_SERVER_ERROR, 578 - Json(json!({"error": "InternalError"})), 579 - ) 580 - .into_response(); 472 + return ApiError::InternalError(None).into_response(); 581 473 } 582 474 }; 583 475 ··· 591 483 .await 592 484 { 593 485 error!("Error inserting user key: {:?}", e); 594 - return ( 595 - StatusCode::INTERNAL_SERVER_ERROR, 596 - Json(json!({"error": "InternalError"})), 597 - ) 598 - .into_response(); 486 + return ApiError::InternalError(None).into_response(); 599 487 } 600 488 601 489 if let Some(key_id) = reserved_key_id ··· 607 495 .await 608 496 { 609 497 error!("Error marking reserved key as used: {:?}", e); 610 - return ( 611 - StatusCode::INTERNAL_SERVER_ERROR, 612 - Json(json!({"error": "InternalError"})), 613 - ) 614 - .into_response(); 498 + return ApiError::InternalError(None).into_response(); 615 499 } 616 500 617 501 let mst = Mst::new(Arc::new(state.block_store.clone())); ··· 619 503 Ok(c) => c, 620 504 Err(e) => { 621 505 error!("Error persisting MST: {:?}", e); 622 - return ( 623 - StatusCode::INTERNAL_SERVER_ERROR, 624 - Json(json!({"error": "InternalError"})), 625 - ) 626 - .into_response(); 506 + return ApiError::InternalError(None).into_response(); 627 507 } 628 508 }; 629 509 let rev = Tid::now(LimitedU32::MIN); ··· 632 512 Ok(result) => result, 633 513 Err(e) => { 634 514 error!("Error creating genesis commit: {:?}", e); 635 - return ( 636 - StatusCode::INTERNAL_SERVER_ERROR, 637 - Json(json!({"error": "InternalError"})), 638 - ) 639 - .into_response(); 515 + return ApiError::InternalError(None).into_response(); 640 516 } 641 517 }; 642 518 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 643 519 Ok(c) => c, 644 520 Err(e) => { 645 521 error!("Error saving genesis commit: {:?}", e); 646 - return ( 647 - StatusCode::INTERNAL_SERVER_ERROR, 648 - Json(json!({"error": "InternalError"})), 649 - ) 650 - .into_response(); 522 + return ApiError::InternalError(None).into_response(); 651 523 } 652 524 }; 653 525 let commit_cid_str = commit_cid.to_string(); ··· 662 534 .await 663 535 { 664 536 error!("Error inserting repo: {:?}", e); 665 - return ( 666 - StatusCode::INTERNAL_SERVER_ERROR, 667 - Json(json!({"error": "InternalError"})), 668 - ) 669 - .into_response(); 537 + return ApiError::InternalError(None).into_response(); 670 538 } 671 539 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; 672 540 if let Err(e) = sqlx::query!( ··· 682 550 .await 683 551 { 684 552 error!("Error inserting user_blocks: {:?}", e); 685 - return ( 686 - StatusCode::INTERNAL_SERVER_ERROR, 687 - Json(json!({"error": "InternalError"})), 688 - ) 689 - .into_response(); 553 + return ApiError::InternalError(None).into_response(); 690 554 } 691 555 692 556 if let Some(ref code) = input.invite_code { ··· 727 591 728 592 if let Err(e) = tx.commit().await { 729 593 error!("Error committing transaction: {:?}", e); 730 - return ( 731 - StatusCode::INTERNAL_SERVER_ERROR, 732 - Json(json!({"error": "InternalError"})), 733 - ) 734 - .into_response(); 594 + return ApiError::InternalError(None).into_response(); 735 595 } 736 596 737 597 if !is_byod_did_web { ··· 819 679 }; 820 680 821 681 Json(CreatePasskeyAccountResponse { 822 - did, 823 - handle, 682 + did: did.into(), 683 + handle: handle.into(), 824 684 setup_token, 825 685 setup_expires_at, 826 686 access_jwt, ··· 831 691 #[derive(Deserialize)] 832 692 #[serde(rename_all = "camelCase")] 833 693 pub struct CompletePasskeySetupInput { 834 - pub did: String, 694 + pub did: Did, 835 695 pub setup_token: String, 836 696 pub passkey_credential: serde_json::Value, 837 697 pub passkey_friendly_name: Option<String>, ··· 840 700 #[derive(Serialize)] 841 701 #[serde(rename_all = "camelCase")] 842 702 pub struct CompletePasskeySetupResponse { 843 - pub did: String, 844 - pub handle: String, 703 + pub did: Did, 704 + pub handle: Handle, 845 705 pub app_password: String, 846 706 pub app_password_name: String, 847 707 } ··· 853 713 let user = sqlx::query!( 854 714 r#"SELECT id, handle, recovery_token, recovery_token_expires_at, password_required 855 715 FROM users WHERE did = $1"#, 856 - input.did 716 + input.did.as_str() 857 717 ) 858 718 .fetch_optional(&state.db) 859 719 .await; ··· 861 721 let user = match user { 862 722 Ok(Some(u)) => u, 863 723 Ok(None) => { 864 - return ( 865 - StatusCode::NOT_FOUND, 866 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 867 - ) 868 - .into_response(); 724 + return ApiError::AccountNotFound.into_response(); 869 725 } 870 726 Err(e) => { 871 727 error!("DB error: {:?}", e); 872 - return ( 873 - StatusCode::INTERNAL_SERVER_ERROR, 874 - Json(json!({"error": "InternalError"})), 875 - ) 876 - .into_response(); 728 + return ApiError::InternalError(None).into_response(); 877 729 } 878 730 }; 879 731 880 732 if user.password_required { 881 - return ( 882 - StatusCode::BAD_REQUEST, 883 - Json(json!({"error": "InvalidAccount", "message": "This account is not a passkey-only account"})), 884 - ) 885 - .into_response(); 733 + return ApiError::InvalidAccount.into_response(); 886 734 } 887 735 888 736 let token_hash = match &user.recovery_token { 889 737 Some(h) => h, 890 738 None => { 891 - return ( 892 - StatusCode::BAD_REQUEST, 893 - Json(json!({"error": "SetupExpired", "message": "Setup has already been completed or expired"})), 894 - ) 895 - .into_response(); 739 + return ApiError::SetupExpired.into_response(); 896 740 } 897 741 }; 898 742 899 743 if let Some(expires_at) = user.recovery_token_expires_at 900 744 && expires_at < Utc::now() 901 745 { 902 - return ( 903 - StatusCode::BAD_REQUEST, 904 - Json(json!({"error": "SetupExpired", "message": "Setup token has expired"})), 905 - ) 906 - .into_response(); 746 + return ApiError::SetupExpired.into_response(); 907 747 } 908 748 909 749 if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) { 910 - return ( 911 - StatusCode::UNAUTHORIZED, 912 - Json(json!({"error": "InvalidToken", "message": "Invalid setup token"})), 913 - ) 914 - .into_response(); 750 + return ApiError::InvalidToken(None).into_response(); 915 751 } 916 752 917 753 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 919 755 Ok(w) => w, 920 756 Err(e) => { 921 757 error!("Failed to create WebAuthn config: {:?}", e); 922 - return ( 923 - StatusCode::INTERNAL_SERVER_ERROR, 924 - Json(json!({"error": "InternalError"})), 925 - ) 926 - .into_response(); 758 + return ApiError::InternalError(None).into_response(); 927 759 } 928 760 }; 929 761 ··· 932 764 { 933 765 Ok(Some(s)) => s, 934 766 Ok(None) => { 935 - return ( 936 - StatusCode::BAD_REQUEST, 937 - Json(json!({"error": "NoChallengeInProgress", "message": "Please start passkey registration first"})), 938 - ) 939 - .into_response(); 767 + return ApiError::NoChallengeInProgress.into_response(); 940 768 } 941 769 Err(e) => { 942 770 error!("Error loading registration state: {:?}", e); 943 - return ( 944 - StatusCode::INTERNAL_SERVER_ERROR, 945 - Json(json!({"error": "InternalError"})), 946 - ) 947 - .into_response(); 771 + return ApiError::InternalError(None).into_response(); 948 772 } 949 773 }; 950 774 ··· 953 777 Ok(c) => c, 954 778 Err(e) => { 955 779 warn!("Failed to parse credential: {:?}", e); 956 - return ( 957 - StatusCode::BAD_REQUEST, 958 - Json( 959 - json!({"error": "InvalidCredential", "message": "Failed to parse credential"}), 960 - ), 961 - ) 962 - .into_response(); 780 + return ApiError::InvalidCredential.into_response(); 963 781 } 964 782 }; 965 783 ··· 967 785 Ok(sk) => sk, 968 786 Err(e) => { 969 787 warn!("Passkey registration failed: {:?}", e); 970 - return ( 971 - StatusCode::BAD_REQUEST, 972 - Json(json!({"error": "RegistrationFailed", "message": "Passkey registration failed"})), 973 - ) 974 - .into_response(); 788 + return ApiError::RegistrationFailed.into_response(); 975 789 } 976 790 }; 977 791 ··· 984 798 .await 985 799 { 986 800 error!("Error saving passkey: {:?}", e); 987 - return ( 988 - StatusCode::INTERNAL_SERVER_ERROR, 989 - Json(json!({"error": "InternalError"})), 990 - ) 991 - .into_response(); 801 + return ApiError::InternalError(None).into_response(); 992 802 } 993 803 994 804 let _ = crate::auth::webauthn::delete_registration_state(&state.db, &input.did).await; ··· 999 809 Ok(h) => h, 1000 810 Err(e) => { 1001 811 error!("Error hashing app password: {:?}", e); 1002 - return ( 1003 - StatusCode::INTERNAL_SERVER_ERROR, 1004 - Json(json!({"error": "InternalError"})), 1005 - ) 1006 - .into_response(); 812 + return ApiError::InternalError(None).into_response(); 1007 813 } 1008 814 }; 1009 815 ··· 1017 823 .await 1018 824 { 1019 825 error!("Error creating app password: {:?}", e); 1020 - return ( 1021 - StatusCode::INTERNAL_SERVER_ERROR, 1022 - Json(json!({"error": "InternalError"})), 1023 - ) 1024 - .into_response(); 826 + return ApiError::InternalError(None).into_response(); 1025 827 } 1026 828 1027 829 if let Err(e) = sqlx::query!( 1028 830 "UPDATE users SET recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $1", 1029 - input.did 831 + input.did.as_str() 1030 832 ) 1031 833 .execute(&state.db) 1032 834 .await ··· 1037 839 info!(did = %input.did, "Passkey-only account setup completed"); 1038 840 1039 841 Json(CompletePasskeySetupResponse { 1040 - did: input.did, 1041 - handle: user.handle, 842 + did: input.did.clone(), 843 + handle: user.handle.into(), 1042 844 app_password, 1043 845 app_password_name, 1044 846 }) ··· 1052 854 let user = sqlx::query!( 1053 855 r#"SELECT handle, recovery_token, recovery_token_expires_at, password_required 1054 856 FROM users WHERE did = $1"#, 1055 - input.did 857 + input.did.as_str() 1056 858 ) 1057 859 .fetch_optional(&state.db) 1058 860 .await; ··· 1060 862 let user = match user { 1061 863 Ok(Some(u)) => u, 1062 864 Ok(None) => { 1063 - return ( 1064 - StatusCode::NOT_FOUND, 1065 - Json(json!({"error": "AccountNotFound"})), 1066 - ) 1067 - .into_response(); 865 + return ApiError::AccountNotFound.into_response(); 1068 866 } 1069 867 Err(e) => { 1070 868 error!("DB error: {:?}", e); 1071 - return ( 1072 - StatusCode::INTERNAL_SERVER_ERROR, 1073 - Json(json!({"error": "InternalError"})), 1074 - ) 1075 - .into_response(); 869 + return ApiError::InternalError(None).into_response(); 1076 870 } 1077 871 }; 1078 872 1079 873 if user.password_required { 1080 - return ( 1081 - StatusCode::BAD_REQUEST, 1082 - Json(json!({"error": "InvalidAccount"})), 1083 - ) 1084 - .into_response(); 874 + return ApiError::InvalidAccount.into_response(); 1085 875 } 1086 876 1087 877 let token_hash = match &user.recovery_token { 1088 878 Some(h) => h, 1089 879 None => { 1090 - return ( 1091 - StatusCode::BAD_REQUEST, 1092 - Json(json!({"error": "SetupExpired"})), 1093 - ) 1094 - .into_response(); 880 + return ApiError::SetupExpired.into_response(); 1095 881 } 1096 882 }; 1097 883 1098 884 if let Some(expires_at) = user.recovery_token_expires_at 1099 885 && expires_at < Utc::now() 1100 886 { 1101 - return ( 1102 - StatusCode::BAD_REQUEST, 1103 - Json(json!({"error": "SetupExpired"})), 1104 - ) 1105 - .into_response(); 887 + return ApiError::SetupExpired.into_response(); 1106 888 } 1107 889 1108 890 if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) { 1109 - return ( 1110 - StatusCode::UNAUTHORIZED, 1111 - Json(json!({"error": "InvalidToken"})), 1112 - ) 1113 - .into_response(); 891 + return ApiError::InvalidToken(None).into_response(); 1114 892 } 1115 893 1116 894 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 1118 896 Ok(w) => w, 1119 897 Err(e) => { 1120 898 error!("Failed to create WebAuthn config: {:?}", e); 1121 - return ( 1122 - StatusCode::INTERNAL_SERVER_ERROR, 1123 - Json(json!({"error": "InternalError"})), 1124 - ) 1125 - .into_response(); 899 + return ApiError::InternalError(None).into_response(); 1126 900 } 1127 901 }; 1128 902 ··· 1146 920 Ok(result) => result, 1147 921 Err(e) => { 1148 922 error!("Failed to start passkey registration: {:?}", e); 1149 - return ( 1150 - StatusCode::INTERNAL_SERVER_ERROR, 1151 - Json(json!({"error": "InternalError"})), 1152 - ) 1153 - .into_response(); 923 + return ApiError::InternalError(None).into_response(); 1154 924 } 1155 925 }; 1156 926 ··· 1158 928 crate::auth::webauthn::save_registration_state(&state.db, &input.did, &reg_state).await 1159 929 { 1160 930 error!("Failed to save registration state: {:?}", e); 1161 - return ( 1162 - StatusCode::INTERNAL_SERVER_ERROR, 1163 - Json(json!({"error": "InternalError"})), 1164 - ) 1165 - .into_response(); 931 + return ApiError::InternalError(None).into_response(); 1166 932 } 1167 933 1168 934 let options = serde_json::to_value(&ccr).unwrap_or(json!({})); ··· 1172 938 #[derive(Deserialize)] 1173 939 #[serde(rename_all = "camelCase")] 1174 940 pub struct StartPasskeyRegistrationInput { 1175 - pub did: String, 941 + pub did: Did, 1176 942 pub setup_token: String, 1177 943 pub friendly_name: Option<String>, 1178 944 } ··· 1194 960 .check_rate_limit(RateLimitKind::PasswordReset, &client_ip) 1195 961 .await 1196 962 { 1197 - return ( 1198 - StatusCode::TOO_MANY_REQUESTS, 1199 - Json(json!({"error": "RateLimitExceeded"})), 1200 - ) 1201 - .into_response(); 963 + return ApiError::RateLimitExceeded(None).into_response(); 1202 964 } 1203 965 1204 966 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 1221 983 let user = match user { 1222 984 Ok(Some(u)) if !u.password_required => u, 1223 985 _ => { 1224 - return Json(json!({"success": true})).into_response(); 986 + return SuccessResponse::ok().into_response(); 1225 987 } 1226 988 }; 1227 989 ··· 1229 991 let recovery_token_hash = match hash(&recovery_token, DEFAULT_COST) { 1230 992 Ok(h) => h, 1231 993 Err(_) => { 1232 - return ( 1233 - StatusCode::INTERNAL_SERVER_ERROR, 1234 - Json(json!({"error": "InternalError"})), 1235 - ) 1236 - .into_response(); 994 + return ApiError::InternalError(None).into_response(); 1237 995 } 1238 996 }; 1239 997 let expires_at = Utc::now() + Duration::hours(1); ··· 1242 1000 "UPDATE users SET recovery_token = $1, recovery_token_expires_at = $2 WHERE did = $3", 1243 1001 recovery_token_hash, 1244 1002 expires_at, 1245 - user.did 1003 + &user.did 1246 1004 ) 1247 1005 .execute(&state.db) 1248 1006 .await 1249 1007 { 1250 1008 error!("Error updating recovery token: {:?}", e); 1251 - return ( 1252 - StatusCode::INTERNAL_SERVER_ERROR, 1253 - Json(json!({"error": "InternalError"})), 1254 - ) 1255 - .into_response(); 1009 + return ApiError::InternalError(None).into_response(); 1256 1010 } 1257 1011 1258 1012 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 1267 1021 crate::comms::enqueue_passkey_recovery(&state.db, user.id, &recovery_url, &hostname).await; 1268 1022 1269 1023 info!(did = %user.did, "Passkey recovery requested"); 1270 - Json(json!({"success": true})).into_response() 1024 + SuccessResponse::ok().into_response() 1271 1025 } 1272 1026 1273 1027 #[derive(Deserialize)] 1274 1028 #[serde(rename_all = "camelCase")] 1275 1029 pub struct RecoverPasskeyAccountInput { 1276 - pub did: String, 1030 + pub did: Did, 1277 1031 pub recovery_token: String, 1278 - pub new_password: String, 1032 + pub new_password: PlainPassword, 1279 1033 } 1280 1034 1281 1035 pub async fn recover_passkey_account( ··· 1283 1037 Json(input): Json<RecoverPasskeyAccountInput>, 1284 1038 ) -> Response { 1285 1039 if let Err(e) = validate_password(&input.new_password) { 1286 - return ( 1287 - StatusCode::BAD_REQUEST, 1288 - Json(json!({ 1289 - "error": "InvalidPassword", 1290 - "message": e.to_string() 1291 - })), 1292 - ) 1293 - .into_response(); 1040 + return ApiError::InvalidRequest(e.to_string()).into_response(); 1294 1041 } 1295 1042 1296 1043 let user = sqlx::query!( 1297 1044 "SELECT id, did, recovery_token, recovery_token_expires_at FROM users WHERE did = $1", 1298 - input.did 1045 + input.did.as_str() 1299 1046 ) 1300 1047 .fetch_optional(&state.db) 1301 1048 .await; ··· 1303 1050 let user = match user { 1304 1051 Ok(Some(u)) => u, 1305 1052 _ => { 1306 - return ( 1307 - StatusCode::NOT_FOUND, 1308 - Json(json!({"error": "InvalidRecoveryLink"})), 1309 - ) 1310 - .into_response(); 1053 + return ApiError::InvalidRecoveryLink.into_response(); 1311 1054 } 1312 1055 }; 1313 1056 1314 1057 let token_hash = match &user.recovery_token { 1315 1058 Some(h) => h, 1316 1059 None => { 1317 - return ( 1318 - StatusCode::BAD_REQUEST, 1319 - Json(json!({"error": "InvalidRecoveryLink"})), 1320 - ) 1321 - .into_response(); 1060 + return ApiError::InvalidRecoveryLink.into_response(); 1322 1061 } 1323 1062 }; 1324 1063 1325 1064 if let Some(expires_at) = user.recovery_token_expires_at 1326 1065 && expires_at < Utc::now() 1327 1066 { 1328 - return ( 1329 - StatusCode::BAD_REQUEST, 1330 - Json(json!({"error": "RecoveryLinkExpired"})), 1331 - ) 1332 - .into_response(); 1067 + return ApiError::RecoveryLinkExpired.into_response(); 1333 1068 } 1334 1069 1335 1070 if !bcrypt::verify(&input.recovery_token, token_hash).unwrap_or(false) { 1336 - return ( 1337 - StatusCode::UNAUTHORIZED, 1338 - Json(json!({"error": "InvalidRecoveryLink"})), 1339 - ) 1340 - .into_response(); 1071 + return ApiError::InvalidRecoveryLink.into_response(); 1341 1072 } 1342 1073 1343 1074 let password_hash = match hash(&input.new_password, DEFAULT_COST) { 1344 1075 Ok(h) => h, 1345 1076 Err(_) => { 1346 - return ( 1347 - StatusCode::INTERNAL_SERVER_ERROR, 1348 - Json(json!({"error": "InternalError"})), 1349 - ) 1350 - .into_response(); 1077 + return ApiError::InternalError(None).into_response(); 1351 1078 } 1352 1079 }; 1353 1080 1354 1081 if let Err(e) = sqlx::query!( 1355 1082 "UPDATE users SET password_hash = $1, password_required = TRUE, recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $2", 1356 1083 password_hash, 1357 - input.did 1084 + input.did.as_str() 1358 1085 ) 1359 1086 .execute(&state.db) 1360 1087 .await 1361 1088 { 1362 1089 error!("Error updating password: {:?}", e); 1363 - return ( 1364 - StatusCode::INTERNAL_SERVER_ERROR, 1365 - Json(json!({"error": "InternalError"})), 1366 - ) 1367 - .into_response(); 1090 + return ApiError::InternalError(None).into_response(); 1368 1091 } 1369 1092 1370 - let deleted = sqlx::query!("DELETE FROM passkeys WHERE did = $1", input.did) 1093 + let deleted = sqlx::query!("DELETE FROM passkeys WHERE did = $1", input.did.as_str()) 1371 1094 .execute(&state.db) 1372 1095 .await; 1373 1096 match deleted { ··· 1382 1105 } 1383 1106 1384 1107 info!(did = %input.did, "Passkey-only account recovered with temporary password"); 1385 - Json(json!({"success": true})).into_response() 1108 + SuccessResponse::ok().into_response() 1386 1109 }
+25 -104
src/api/server/passkeys.rs
··· 1 + use crate::api::EmptyResponse; 2 + use crate::api::error::ApiError; 1 3 use crate::auth::BearerAuth; 2 4 use crate::auth::webauthn::{ 3 5 self, WebAuthnConfig, delete_passkey as db_delete_passkey, delete_registration_state, ··· 8 10 use axum::{ 9 11 Json, 10 12 extract::State, 11 - http::StatusCode, 12 13 response::{IntoResponse, Response}, 13 14 }; 14 15 use serde::{Deserialize, Serialize}; 15 - use serde_json::json; 16 16 use tracing::{error, info, warn}; 17 17 use webauthn_rs::prelude::*; 18 18 19 - fn get_webauthn() -> Result<WebAuthnConfig, (StatusCode, Json<serde_json::Value>)> { 19 + fn get_webauthn() -> Result<WebAuthnConfig, ApiError> { 20 20 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 21 21 WebAuthnConfig::new(&hostname).map_err(|e| { 22 22 error!("Failed to create WebAuthn config: {}", e); 23 - ( 24 - StatusCode::INTERNAL_SERVER_ERROR, 25 - Json(json!({"error": "InternalError", "message": "WebAuthn configuration failed"})), 26 - ) 23 + ApiError::InternalError(Some("WebAuthn configuration failed".into())) 27 24 }) 28 25 } 29 26 ··· 49 46 Err(e) => return e.into_response(), 50 47 }; 51 48 52 - let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", auth.0.did) 49 + let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", &*auth.0.did) 53 50 .fetch_optional(&state.db) 54 51 .await; 55 52 56 53 let handle = match user { 57 54 Ok(Some(row)) => row.handle, 58 55 Ok(None) => { 59 - return ( 60 - StatusCode::NOT_FOUND, 61 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 62 - ) 63 - .into_response(); 56 + return ApiError::AccountNotFound.into_response(); 64 57 } 65 58 Err(e) => { 66 59 error!("DB error fetching user: {:?}", e); 67 - return ( 68 - StatusCode::INTERNAL_SERVER_ERROR, 69 - Json(json!({"error": "InternalError"})), 70 - ) 71 - .into_response(); 60 + return ApiError::InternalError(None).into_response(); 72 61 } 73 62 }; 74 63 ··· 76 65 Ok(passkeys) => passkeys, 77 66 Err(e) => { 78 67 error!("DB error fetching existing passkeys: {:?}", e); 79 - return ( 80 - StatusCode::INTERNAL_SERVER_ERROR, 81 - Json(json!({"error": "InternalError"})), 82 - ) 83 - .into_response(); 68 + return ApiError::InternalError(None).into_response(); 84 69 } 85 70 }; 86 71 ··· 100 85 Ok(result) => result, 101 86 Err(e) => { 102 87 error!("Failed to start passkey registration: {}", e); 103 - return ( 104 - StatusCode::INTERNAL_SERVER_ERROR, 105 - Json(json!({"error": "InternalError", "message": "Failed to start registration"})), 106 - ) 88 + return ApiError::InternalError(Some("Failed to start registration".into())) 107 89 .into_response(); 108 90 } 109 91 }; 110 92 111 93 if let Err(e) = save_registration_state(&state.db, &auth.0.did, &reg_state).await { 112 94 error!("Failed to save registration state: {:?}", e); 113 - return ( 114 - StatusCode::INTERNAL_SERVER_ERROR, 115 - Json(json!({"error": "InternalError"})), 116 - ) 117 - .into_response(); 95 + return ApiError::InternalError(None).into_response(); 118 96 } 119 97 120 - let options = serde_json::to_value(&ccr).unwrap_or(json!({})); 98 + let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 121 99 122 100 info!(did = %auth.0.did, "Passkey registration started"); 123 101 ··· 151 129 let reg_state = match load_registration_state(&state.db, &auth.0.did).await { 152 130 Ok(Some(state)) => state, 153 131 Ok(None) => { 154 - return ( 155 - StatusCode::BAD_REQUEST, 156 - Json(json!({ 157 - "error": "NoRegistrationInProgress", 158 - "message": "No registration in progress. Call startPasskeyRegistration first." 159 - })), 160 - ) 161 - .into_response(); 132 + return ApiError::NoRegistrationInProgress.into_response(); 162 133 } 163 134 Err(e) => { 164 135 error!("DB error loading registration state: {:?}", e); 165 - return ( 166 - StatusCode::INTERNAL_SERVER_ERROR, 167 - Json(json!({"error": "InternalError"})), 168 - ) 169 - .into_response(); 136 + return ApiError::InternalError(None).into_response(); 170 137 } 171 138 }; 172 139 ··· 174 141 Ok(c) => c, 175 142 Err(e) => { 176 143 warn!("Failed to parse credential: {:?}", e); 177 - return ( 178 - StatusCode::BAD_REQUEST, 179 - Json(json!({ 180 - "error": "InvalidCredential", 181 - "message": "Failed to parse credential response" 182 - })), 183 - ) 184 - .into_response(); 144 + return ApiError::InvalidCredential.into_response(); 185 145 } 186 146 }; 187 147 ··· 189 149 Ok(pk) => pk, 190 150 Err(e) => { 191 151 warn!("Failed to finish passkey registration: {}", e); 192 - return ( 193 - StatusCode::BAD_REQUEST, 194 - Json(json!({ 195 - "error": "RegistrationFailed", 196 - "message": "Failed to verify passkey registration" 197 - })), 198 - ) 199 - .into_response(); 152 + return ApiError::RegistrationFailed.into_response(); 200 153 } 201 154 }; 202 155 ··· 211 164 Ok(id) => id, 212 165 Err(e) => { 213 166 error!("Failed to save passkey: {:?}", e); 214 - return ( 215 - StatusCode::INTERNAL_SERVER_ERROR, 216 - Json(json!({"error": "InternalError"})), 217 - ) 218 - .into_response(); 167 + return ApiError::InternalError(None).into_response(); 219 168 } 220 169 }; 221 170 ··· 258 207 Ok(pks) => pks, 259 208 Err(e) => { 260 209 error!("DB error fetching passkeys: {:?}", e); 261 - return ( 262 - StatusCode::INTERNAL_SERVER_ERROR, 263 - Json(json!({"error": "InternalError"})), 264 - ) 265 - .into_response(); 210 + return ApiError::InternalError(None).into_response(); 266 211 } 267 212 }; 268 213 ··· 306 251 let id: uuid::Uuid = match input.id.parse() { 307 252 Ok(id) => id, 308 253 Err(_) => { 309 - return ( 310 - StatusCode::BAD_REQUEST, 311 - Json(json!({"error": "InvalidId", "message": "Invalid passkey ID"})), 312 - ) 313 - .into_response(); 254 + return ApiError::InvalidId.into_response(); 314 255 } 315 256 }; 316 257 317 258 match db_delete_passkey(&state.db, id, &auth.0.did).await { 318 259 Ok(true) => { 319 260 info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted"); 320 - (StatusCode::OK, Json(json!({}))).into_response() 261 + EmptyResponse::ok().into_response() 321 262 } 322 - Ok(false) => ( 323 - StatusCode::NOT_FOUND, 324 - Json(json!({"error": "PasskeyNotFound", "message": "Passkey not found"})), 325 - ) 326 - .into_response(), 263 + Ok(false) => ApiError::PasskeyNotFound.into_response(), 327 264 Err(e) => { 328 265 error!("DB error deleting passkey: {:?}", e); 329 - ( 330 - StatusCode::INTERNAL_SERVER_ERROR, 331 - Json(json!({"error": "InternalError"})), 332 - ) 333 - .into_response() 266 + ApiError::InternalError(None).into_response() 334 267 } 335 268 } 336 269 } ··· 350 283 let id: uuid::Uuid = match input.id.parse() { 351 284 Ok(id) => id, 352 285 Err(_) => { 353 - return ( 354 - StatusCode::BAD_REQUEST, 355 - Json(json!({"error": "InvalidId", "message": "Invalid passkey ID"})), 356 - ) 357 - .into_response(); 286 + return ApiError::InvalidId.into_response(); 358 287 } 359 288 }; 360 289 361 290 match db_update_passkey_name(&state.db, id, &auth.0.did, &input.friendly_name).await { 362 291 Ok(true) => { 363 292 info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed"); 364 - (StatusCode::OK, Json(json!({}))).into_response() 293 + EmptyResponse::ok().into_response() 365 294 } 366 - Ok(false) => ( 367 - StatusCode::NOT_FOUND, 368 - Json(json!({"error": "PasskeyNotFound", "message": "Passkey not found"})), 369 - ) 370 - .into_response(), 295 + Ok(false) => ApiError::PasskeyNotFound.into_response(), 371 296 Err(e) => { 372 297 error!("DB error updating passkey: {:?}", e); 373 - ( 374 - StatusCode::INTERNAL_SERVER_ERROR, 375 - Json(json!({"error": "InternalError"})), 376 - ) 377 - .into_response() 298 + ApiError::InternalError(None).into_response() 378 299 } 379 300 } 380 301 }
+55 -214
src/api/server/password.rs
··· 1 + use crate::api::error::ApiError; 2 + use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 1 3 use crate::auth::BearerAuth; 2 4 use crate::state::{AppState, RateLimitKind}; 5 + use crate::types::PlainPassword; 3 6 use crate::validation::validate_password; 4 7 use axum::{ 5 8 Json, 6 9 extract::State, 7 - http::{HeaderMap, StatusCode}, 10 + http::HeaderMap, 8 11 response::{IntoResponse, Response}, 9 12 }; 10 13 use bcrypt::{DEFAULT_COST, hash, verify}; 11 14 use chrono::{Duration, Utc}; 12 15 use serde::Deserialize; 13 - use serde_json::json; 14 16 use tracing::{error, info, warn}; 15 17 use uuid::Uuid; 16 18 ··· 49 51 .await 50 52 { 51 53 warn!(ip = %client_ip, "Password reset rate limit exceeded"); 52 - return ( 53 - StatusCode::TOO_MANY_REQUESTS, 54 - Json(json!({ 55 - "error": "RateLimitExceeded", 56 - "message": "Too many password reset requests. Please try again later." 57 - })), 58 - ) 59 - .into_response(); 54 + return ApiError::RateLimitExceeded(None).into_response(); 60 55 } 61 56 let identifier = input.email.trim(); 62 57 if identifier.is_empty() { 63 - return ( 64 - StatusCode::BAD_REQUEST, 65 - Json(json!({"error": "InvalidRequest", "message": "email or handle is required"})), 66 - ) 67 - .into_response(); 58 + return ApiError::InvalidRequest("email or handle is required".into()).into_response(); 68 59 } 69 60 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 70 61 let normalized = identifier.to_lowercase(); ··· 85 76 Ok(Some(row)) => row.id, 86 77 Ok(None) => { 87 78 info!("Password reset requested for unknown identifier"); 88 - return (StatusCode::OK, Json(json!({}))).into_response(); 79 + return EmptyResponse::ok().into_response(); 89 80 } 90 81 Err(e) => { 91 82 error!("DB error in request_password_reset: {:?}", e); 92 - return ( 93 - StatusCode::INTERNAL_SERVER_ERROR, 94 - Json(json!({"error": "InternalError"})), 95 - ) 96 - .into_response(); 83 + return ApiError::InternalError(None).into_response(); 97 84 } 98 85 }; 99 86 let code = generate_reset_code(); ··· 108 95 .await; 109 96 if let Err(e) = update { 110 97 error!("DB error setting reset code: {:?}", e); 111 - return ( 112 - StatusCode::INTERNAL_SERVER_ERROR, 113 - Json(json!({"error": "InternalError"})), 114 - ) 115 - .into_response(); 98 + return ApiError::InternalError(None).into_response(); 116 99 } 117 100 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 118 101 if let Err(e) = crate::comms::enqueue_password_reset(&state.db, user_id, &code, &hostname).await ··· 120 103 warn!("Failed to enqueue password reset notification: {:?}", e); 121 104 } 122 105 info!("Password reset requested for user {}", user_id); 123 - (StatusCode::OK, Json(json!({}))).into_response() 106 + EmptyResponse::ok().into_response() 124 107 } 125 108 126 109 #[derive(Deserialize)] 127 110 pub struct ResetPasswordInput { 128 111 pub token: String, 129 - pub password: String, 112 + pub password: PlainPassword, 130 113 } 131 114 132 115 pub async fn reset_password( ··· 140 123 .await 141 124 { 142 125 warn!(ip = %client_ip, "Reset password rate limit exceeded"); 143 - return ( 144 - StatusCode::TOO_MANY_REQUESTS, 145 - Json(json!({ 146 - "error": "RateLimitExceeded", 147 - "message": "Too many requests. Please try again later." 148 - })), 149 - ) 150 - .into_response(); 126 + return ApiError::RateLimitExceeded(None).into_response(); 151 127 } 152 128 let token = input.token.trim(); 153 129 let password = &input.password; 154 130 if token.is_empty() { 155 - return ( 156 - StatusCode::BAD_REQUEST, 157 - Json(json!({"error": "InvalidToken", "message": "token is required"})), 158 - ) 159 - .into_response(); 131 + return ApiError::InvalidToken(None).into_response(); 160 132 } 161 133 if password.is_empty() { 162 - return ( 163 - StatusCode::BAD_REQUEST, 164 - Json(json!({"error": "InvalidRequest", "message": "password is required"})), 165 - ) 166 - .into_response(); 134 + return ApiError::InvalidRequest("password is required".into()).into_response(); 167 135 } 168 136 if let Err(e) = validate_password(password) { 169 - return ( 170 - StatusCode::BAD_REQUEST, 171 - Json(json!({ 172 - "error": "InvalidPassword", 173 - "message": e.to_string() 174 - })), 175 - ) 176 - .into_response(); 137 + return ApiError::InvalidRequest(e.to_string()).into_response(); 177 138 } 178 139 let user = sqlx::query!( 179 140 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", ··· 187 148 (row.id, expires) 188 149 } 189 150 Ok(None) => { 190 - return ( 191 - StatusCode::BAD_REQUEST, 192 - Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 193 - ) 194 - .into_response(); 151 + return ApiError::InvalidToken(None).into_response(); 195 152 } 196 153 Err(e) => { 197 154 error!("DB error in reset_password: {:?}", e); 198 - return ( 199 - StatusCode::INTERNAL_SERVER_ERROR, 200 - Json(json!({"error": "InternalError"})), 201 - ) 202 - .into_response(); 155 + return ApiError::InternalError(None).into_response(); 203 156 } 204 157 }; 205 158 if let Some(exp) = expires_at { ··· 213 166 { 214 167 error!("Failed to clear expired reset code: {:?}", e); 215 168 } 216 - return ( 217 - StatusCode::BAD_REQUEST, 218 - Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 219 - ) 220 - .into_response(); 169 + return ApiError::ExpiredToken(None).into_response(); 221 170 } 222 171 } else { 223 - return ( 224 - StatusCode::BAD_REQUEST, 225 - Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 226 - ) 227 - .into_response(); 172 + return ApiError::InvalidToken(None).into_response(); 228 173 } 229 174 let password_clone = password.to_string(); 230 175 let password_hash = ··· 232 177 Ok(Ok(h)) => h, 233 178 Ok(Err(e)) => { 234 179 error!("Failed to hash password: {:?}", e); 235 - return ( 236 - StatusCode::INTERNAL_SERVER_ERROR, 237 - Json(json!({"error": "InternalError"})), 238 - ) 239 - .into_response(); 180 + return ApiError::InternalError(None).into_response(); 240 181 } 241 182 Err(e) => { 242 183 error!("Failed to spawn blocking task: {:?}", e); 243 - return ( 244 - StatusCode::INTERNAL_SERVER_ERROR, 245 - Json(json!({"error": "InternalError"})), 246 - ) 247 - .into_response(); 184 + return ApiError::InternalError(None).into_response(); 248 185 } 249 186 }; 250 187 let mut tx = match state.db.begin().await { 251 188 Ok(tx) => tx, 252 189 Err(e) => { 253 190 error!("Failed to begin transaction: {:?}", e); 254 - return ( 255 - StatusCode::INTERNAL_SERVER_ERROR, 256 - Json(json!({"error": "InternalError"})), 257 - ) 258 - .into_response(); 191 + return ApiError::InternalError(None).into_response(); 259 192 } 260 193 }; 261 194 if let Err(e) = sqlx::query!( ··· 267 200 .await 268 201 { 269 202 error!("DB error updating password: {:?}", e); 270 - return ( 271 - StatusCode::INTERNAL_SERVER_ERROR, 272 - Json(json!({"error": "InternalError"})), 273 - ) 274 - .into_response(); 203 + return ApiError::InternalError(None).into_response(); 275 204 } 276 205 let user_did = match sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", user_id) 277 206 .fetch_one(&mut *tx) ··· 280 209 Ok(did) => did, 281 210 Err(e) => { 282 211 error!("Failed to get DID for user {}: {:?}", user_id, e); 283 - return ( 284 - StatusCode::INTERNAL_SERVER_ERROR, 285 - Json(json!({"error": "InternalError"})), 286 - ) 287 - .into_response(); 212 + return ApiError::InternalError(None).into_response(); 288 213 } 289 214 }; 290 215 let session_jtis: Vec<String> = match sqlx::query_scalar!( ··· 308 233 "Failed to invalidate sessions after password reset: {:?}", 309 234 e 310 235 ); 311 - return ( 312 - StatusCode::INTERNAL_SERVER_ERROR, 313 - Json(json!({"error": "InternalError"})), 314 - ) 315 - .into_response(); 236 + return ApiError::InternalError(None).into_response(); 316 237 } 317 238 if let Err(e) = tx.commit().await { 318 239 error!("Failed to commit password reset transaction: {:?}", e); 319 - return ( 320 - StatusCode::INTERNAL_SERVER_ERROR, 321 - Json(json!({"error": "InternalError"})), 322 - ) 323 - .into_response(); 240 + return ApiError::InternalError(None).into_response(); 324 241 } 325 242 for jti in session_jtis { 326 243 let cache_key = format!("auth:session:{}:{}", user_did, jti); ··· 332 249 } 333 250 } 334 251 info!("Password reset completed for user {}", user_id); 335 - (StatusCode::OK, Json(json!({}))).into_response() 252 + EmptyResponse::ok().into_response() 336 253 } 337 254 338 255 #[derive(Deserialize)] 339 256 #[serde(rename_all = "camelCase")] 340 257 pub struct ChangePasswordInput { 341 - pub current_password: String, 342 - pub new_password: String, 258 + pub current_password: PlainPassword, 259 + pub new_password: PlainPassword, 343 260 } 344 261 345 262 pub async fn change_password( ··· 355 272 let current_password = &input.current_password; 356 273 let new_password = &input.new_password; 357 274 if current_password.is_empty() { 358 - return ( 359 - StatusCode::BAD_REQUEST, 360 - Json(json!({"error": "InvalidRequest", "message": "currentPassword is required"})), 361 - ) 362 - .into_response(); 275 + return ApiError::InvalidRequest("currentPassword is required".into()).into_response(); 363 276 } 364 277 if new_password.is_empty() { 365 - return ( 366 - StatusCode::BAD_REQUEST, 367 - Json(json!({"error": "InvalidRequest", "message": "newPassword is required"})), 368 - ) 369 - .into_response(); 278 + return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 370 279 } 371 280 if let Err(e) = validate_password(new_password) { 372 - return ( 373 - StatusCode::BAD_REQUEST, 374 - Json(json!({ 375 - "error": "InvalidPassword", 376 - "message": e.to_string() 377 - })), 378 - ) 379 - .into_response(); 281 + return ApiError::InvalidRequest(e.to_string()).into_response(); 380 282 } 381 283 let user = 382 284 sqlx::query_as::<_, (Uuid, String)>("SELECT id, password_hash FROM users WHERE did = $1") ··· 386 288 let (user_id, password_hash) = match user { 387 289 Ok(Some(row)) => row, 388 290 Ok(None) => { 389 - return ( 390 - StatusCode::NOT_FOUND, 391 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 392 - ) 393 - .into_response(); 291 + return ApiError::AccountNotFound.into_response(); 394 292 } 395 293 Err(e) => { 396 294 error!("DB error in change_password: {:?}", e); 397 - return ( 398 - StatusCode::INTERNAL_SERVER_ERROR, 399 - Json(json!({"error": "InternalError"})), 400 - ) 401 - .into_response(); 295 + return ApiError::InternalError(None).into_response(); 402 296 } 403 297 }; 404 298 let valid = match verify(current_password, &password_hash) { 405 299 Ok(v) => v, 406 300 Err(e) => { 407 301 error!("Password verification error: {:?}", e); 408 - return ( 409 - StatusCode::INTERNAL_SERVER_ERROR, 410 - Json(json!({"error": "InternalError"})), 411 - ) 412 - .into_response(); 302 + return ApiError::InternalError(None).into_response(); 413 303 } 414 304 }; 415 305 if !valid { 416 - return ( 417 - StatusCode::UNAUTHORIZED, 418 - Json(json!({"error": "InvalidPassword", "message": "Current password is incorrect"})), 419 - ) 420 - .into_response(); 306 + return ApiError::InvalidPassword("Current password is incorrect".into()).into_response(); 421 307 } 422 308 let new_password_clone = new_password.to_string(); 423 309 let new_hash = ··· 425 311 Ok(Ok(h)) => h, 426 312 Ok(Err(e)) => { 427 313 error!("Failed to hash password: {:?}", e); 428 - return ( 429 - StatusCode::INTERNAL_SERVER_ERROR, 430 - Json(json!({"error": "InternalError"})), 431 - ) 432 - .into_response(); 314 + return ApiError::InternalError(None).into_response(); 433 315 } 434 316 Err(e) => { 435 317 error!("Failed to spawn blocking task: {:?}", e); 436 - return ( 437 - StatusCode::INTERNAL_SERVER_ERROR, 438 - Json(json!({"error": "InternalError"})), 439 - ) 440 - .into_response(); 318 + return ApiError::InternalError(None).into_response(); 441 319 } 442 320 }; 443 321 if let Err(e) = sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2") ··· 447 325 .await 448 326 { 449 327 error!("DB error updating password: {:?}", e); 450 - return ( 451 - StatusCode::INTERNAL_SERVER_ERROR, 452 - Json(json!({"error": "InternalError"})), 453 - ) 454 - .into_response(); 328 + return ApiError::InternalError(None).into_response(); 455 329 } 456 - info!(did = %auth.0.did, "Password changed successfully"); 457 - (StatusCode::OK, Json(json!({}))).into_response() 330 + info!(did = %&auth.0.did, "Password changed successfully"); 331 + EmptyResponse::ok().into_response() 458 332 } 459 333 460 334 pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 461 335 let user = sqlx::query!( 462 336 "SELECT password_hash IS NOT NULL as has_password FROM users WHERE did = $1", 463 - auth.0.did 337 + &auth.0.did 464 338 ) 465 339 .fetch_optional(&state.db) 466 340 .await; 467 341 468 342 match user { 469 - Ok(Some(row)) => { 470 - Json(json!({"hasPassword": row.has_password.unwrap_or(false)})).into_response() 471 - } 472 - Ok(None) => ( 473 - StatusCode::NOT_FOUND, 474 - Json(json!({"error": "AccountNotFound"})), 475 - ) 476 - .into_response(), 343 + Ok(Some(row)) => HasPasswordResponse::new(row.has_password.unwrap_or(false)).into_response(), 344 + Ok(None) => ApiError::AccountNotFound.into_response(), 477 345 Err(e) => { 478 346 error!("DB error: {:?}", e); 479 - ( 480 - StatusCode::INTERNAL_SERVER_ERROR, 481 - Json(json!({"error": "InternalError"})), 482 - ) 483 - .into_response() 347 + ApiError::InternalError(None).into_response() 484 348 } 485 349 } 486 350 } ··· 504 368 let has_passkeys = 505 369 crate::api::server::passkeys::has_passkeys_for_user_db(&state.db, &auth.0.did).await; 506 370 if !has_passkeys { 507 - return ( 508 - StatusCode::BAD_REQUEST, 509 - Json(json!({ 510 - "error": "NoPasskeys", 511 - "message": "You must have at least one passkey registered before removing your password" 512 - })), 371 + return ApiError::InvalidRequest( 372 + "You must have at least one passkey registered before removing your password".into(), 513 373 ) 514 - .into_response(); 374 + .into_response(); 515 375 } 516 376 517 377 let user = sqlx::query!( 518 378 "SELECT id, password_hash FROM users WHERE did = $1", 519 - auth.0.did 379 + &auth.0.did 520 380 ) 521 381 .fetch_optional(&state.db) 522 382 .await; ··· 524 384 let user = match user { 525 385 Ok(Some(u)) => u, 526 386 Ok(None) => { 527 - return ( 528 - StatusCode::NOT_FOUND, 529 - Json(json!({"error": "AccountNotFound"})), 530 - ) 531 - .into_response(); 387 + return ApiError::AccountNotFound.into_response(); 532 388 } 533 389 Err(e) => { 534 390 error!("DB error: {:?}", e); 535 - return ( 536 - StatusCode::INTERNAL_SERVER_ERROR, 537 - Json(json!({"error": "InternalError"})), 538 - ) 539 - .into_response(); 391 + return ApiError::InternalError(None).into_response(); 540 392 } 541 393 }; 542 394 543 395 if user.password_hash.is_none() { 544 - return ( 545 - StatusCode::BAD_REQUEST, 546 - Json(json!({ 547 - "error": "NoPassword", 548 - "message": "Account already has no password" 549 - })), 550 - ) 551 - .into_response(); 396 + return ApiError::InvalidRequest("Account already has no password".into()).into_response(); 552 397 } 553 398 554 399 if let Err(e) = sqlx::query!( ··· 559 404 .await 560 405 { 561 406 error!("DB error removing password: {:?}", e); 562 - return ( 563 - StatusCode::INTERNAL_SERVER_ERROR, 564 - Json(json!({"error": "InternalError"})), 565 - ) 566 - .into_response(); 407 + return ApiError::InternalError(None).into_response(); 567 408 } 568 409 569 - info!(did = %auth.0.did, "Password removed - account is now passkey-only"); 570 - (StatusCode::OK, Json(json!({"success": true}))).into_response() 410 + info!(did = %&auth.0.did, "Password removed - account is now passkey-only"); 411 + SuccessResponse::ok().into_response() 571 412 }
+50 -147
src/api/server/reauth.rs
··· 1 + use crate::api::error::ApiError; 1 2 use axum::{ 2 3 Json, 3 4 extract::State, ··· 6 7 }; 7 8 use chrono::{DateTime, Utc}; 8 9 use serde::{Deserialize, Serialize}; 9 - use serde_json::json; 10 10 use sqlx::PgPool; 11 11 use tracing::{error, info, warn}; 12 12 13 13 use crate::auth::BearerAuth; 14 14 use crate::state::{AppState, RateLimitKind}; 15 + use crate::types::PlainPassword; 15 16 16 17 const REAUTH_WINDOW_SECONDS: i64 = 300; 17 18 ··· 26 27 pub async fn get_reauth_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 27 28 let session = sqlx::query!( 28 29 "SELECT last_reauth_at FROM session_tokens WHERE did = $1 ORDER BY created_at DESC LIMIT 1", 29 - auth.0.did 30 + &auth.0.did 30 31 ) 31 32 .fetch_optional(&state.db) 32 33 .await; ··· 36 37 Ok(None) => None, 37 38 Err(e) => { 38 39 error!("DB error: {:?}", e); 39 - return ( 40 - StatusCode::INTERNAL_SERVER_ERROR, 41 - Json(json!({"error": "InternalError"})), 42 - ) 43 - .into_response(); 40 + return ApiError::InternalError(None).into_response(); 44 41 } 45 42 }; 46 43 ··· 58 55 #[derive(Deserialize)] 59 56 #[serde(rename_all = "camelCase")] 60 57 pub struct PasswordReauthInput { 61 - pub password: String, 58 + pub password: PlainPassword, 62 59 } 63 60 64 61 #[derive(Serialize)] ··· 72 69 auth: BearerAuth, 73 70 Json(input): Json<PasswordReauthInput>, 74 71 ) -> Response { 75 - let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", auth.0.did) 72 + let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", &*&auth.0.did) 76 73 .fetch_optional(&state.db) 77 74 .await; 78 75 79 76 let password_hash = match user { 80 77 Ok(Some(row)) => row.password_hash, 81 78 Ok(None) => { 82 - return ( 83 - StatusCode::NOT_FOUND, 84 - Json(json!({"error": "AccountNotFound"})), 85 - ) 86 - .into_response(); 79 + return ApiError::AccountNotFound.into_response(); 87 80 } 88 81 Err(e) => { 89 82 error!("DB error: {:?}", e); 90 - return ( 91 - StatusCode::INTERNAL_SERVER_ERROR, 92 - Json(json!({"error": "InternalError"})), 93 - ) 94 - .into_response(); 83 + return ApiError::InternalError(None).into_response(); 95 84 } 96 85 }; 97 86 ··· 105 94 "SELECT ap.password_hash FROM app_passwords ap 106 95 JOIN users u ON ap.user_id = u.id 107 96 WHERE u.did = $1", 108 - auth.0.did 97 + &auth.0.did 109 98 ) 110 99 .fetch_all(&state.db) 111 100 .await ··· 116 105 .any(|ap| bcrypt::verify(&input.password, &ap.password_hash).unwrap_or(false)); 117 106 118 107 if !app_password_valid { 119 - warn!(did = %auth.0.did, "Re-auth failed: invalid password"); 120 - return ( 121 - StatusCode::UNAUTHORIZED, 122 - Json(json!({ 123 - "error": "InvalidPassword", 124 - "message": "Password is incorrect" 125 - })), 126 - ) 127 - .into_response(); 108 + warn!(did = %&auth.0.did, "Re-auth failed: invalid password"); 109 + return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 128 110 } 129 111 } 130 112 131 113 match update_last_reauth_cached(&state.db, &state.cache, &auth.0.did).await { 132 114 Ok(reauthed_at) => { 133 - info!(did = %auth.0.did, "Re-auth successful via password"); 115 + info!(did = %&auth.0.did, "Re-auth successful via password"); 134 116 Json(ReauthResponse { reauthed_at }).into_response() 135 117 } 136 118 Err(e) => { 137 119 error!("DB error updating reauth: {:?}", e); 138 - ( 139 - StatusCode::INTERNAL_SERVER_ERROR, 140 - Json(json!({"error": "InternalError"})), 141 - ) 142 - .into_response() 120 + ApiError::InternalError(None).into_response() 143 121 } 144 122 } 145 123 } ··· 159 137 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 160 138 .await 161 139 { 162 - warn!(did = %auth.0.did, "TOTP verification rate limit exceeded"); 163 - return ( 164 - StatusCode::TOO_MANY_REQUESTS, 165 - Json(json!({ 166 - "error": "RateLimitExceeded", 167 - "message": "Too many verification attempts. Please try again in a few minutes." 168 - })), 169 - ) 170 - .into_response(); 140 + warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 141 + return ApiError::RateLimitExceeded(Some("Too many verification attempts. Please try again in a few minutes.".into(),)) 142 + .into_response(); 171 143 } 172 144 173 145 let valid = ··· 175 147 .await; 176 148 177 149 if !valid { 178 - warn!(did = %auth.0.did, "Re-auth failed: invalid TOTP code"); 179 - return ( 180 - StatusCode::UNAUTHORIZED, 181 - Json(json!({ 182 - "error": "InvalidCode", 183 - "message": "Invalid TOTP or backup code" 184 - })), 185 - ) 186 - .into_response(); 150 + warn!(did = %&auth.0.did, "Re-auth failed: invalid TOTP code"); 151 + return ApiError::InvalidCode(Some("Invalid TOTP or backup code".into())).into_response(); 187 152 } 188 153 189 154 match update_last_reauth_cached(&state.db, &state.cache, &auth.0.did).await { 190 155 Ok(reauthed_at) => { 191 - info!(did = %auth.0.did, "Re-auth successful via TOTP"); 156 + info!(did = %&auth.0.did, "Re-auth successful via TOTP"); 192 157 Json(ReauthResponse { reauthed_at }).into_response() 193 158 } 194 159 Err(e) => { 195 160 error!("DB error updating reauth: {:?}", e); 196 - ( 197 - StatusCode::INTERNAL_SERVER_ERROR, 198 - Json(json!({"error": "InternalError"})), 199 - ) 200 - .into_response() 161 + ApiError::InternalError(None).into_response() 201 162 } 202 163 } 203 164 } ··· 216 177 Ok(pks) => pks, 217 178 Err(e) => { 218 179 error!("Failed to get passkeys: {:?}", e); 219 - return ( 220 - StatusCode::INTERNAL_SERVER_ERROR, 221 - Json(json!({"error": "InternalError"})), 222 - ) 223 - .into_response(); 180 + return ApiError::InternalError(None).into_response(); 224 181 } 225 182 }; 226 183 227 184 if stored_passkeys.is_empty() { 228 - return ( 229 - StatusCode::BAD_REQUEST, 230 - Json(json!({ 231 - "error": "NoPasskeys", 232 - "message": "No passkeys registered for this account" 233 - })), 234 - ) 235 - .into_response(); 185 + return ApiError::NoPasskeys.into_response(); 236 186 } 237 187 238 188 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys ··· 241 191 .collect(); 242 192 243 193 if passkeys.is_empty() { 244 - return ( 245 - StatusCode::INTERNAL_SERVER_ERROR, 246 - Json(json!({"error": "InternalError", "message": "Failed to load passkeys"})), 247 - ) 248 - .into_response(); 194 + return ApiError::InternalError(Some("Failed to load passkeys".into())).into_response(); 249 195 } 250 196 251 197 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 252 198 Ok(w) => w, 253 199 Err(e) => { 254 200 error!("Failed to create WebAuthn config: {:?}", e); 255 - return ( 256 - StatusCode::INTERNAL_SERVER_ERROR, 257 - Json(json!({"error": "InternalError"})), 258 - ) 259 - .into_response(); 201 + return ApiError::InternalError(None).into_response(); 260 202 } 261 203 }; 262 204 ··· 264 206 Ok(result) => result, 265 207 Err(e) => { 266 208 error!("Failed to start passkey authentication: {:?}", e); 267 - return ( 268 - StatusCode::INTERNAL_SERVER_ERROR, 269 - Json(json!({"error": "InternalError"})), 270 - ) 271 - .into_response(); 209 + return ApiError::InternalError(None).into_response(); 272 210 } 273 211 }; 274 212 ··· 276 214 crate::auth::webauthn::save_authentication_state(&state.db, &auth.0.did, &auth_state).await 277 215 { 278 216 error!("Failed to save authentication state: {:?}", e); 279 - return ( 280 - StatusCode::INTERNAL_SERVER_ERROR, 281 - Json(json!({"error": "InternalError"})), 282 - ) 283 - .into_response(); 217 + return ApiError::InternalError(None).into_response(); 284 218 } 285 219 286 - let options = serde_json::to_value(&rcr).unwrap_or(json!({})); 220 + let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 287 221 Json(PasskeyReauthStartResponse { options }).into_response() 288 222 } 289 223 ··· 304 238 match crate::auth::webauthn::load_authentication_state(&state.db, &auth.0.did).await { 305 239 Ok(Some(s)) => s, 306 240 Ok(None) => { 307 - return ( 308 - StatusCode::BAD_REQUEST, 309 - Json(json!({ 310 - "error": "NoChallengeInProgress", 311 - "message": "No passkey authentication in progress or challenge expired" 312 - })), 313 - ) 314 - .into_response(); 241 + return ApiError::NoChallengeInProgress.into_response(); 315 242 } 316 243 Err(e) => { 317 244 error!("Failed to load authentication state: {:?}", e); 318 - return ( 319 - StatusCode::INTERNAL_SERVER_ERROR, 320 - Json(json!({"error": "InternalError"})), 321 - ) 322 - .into_response(); 245 + return ApiError::InternalError(None).into_response(); 323 246 } 324 247 }; 325 248 ··· 328 251 Ok(c) => c, 329 252 Err(e) => { 330 253 warn!("Failed to parse credential: {:?}", e); 331 - return ( 332 - StatusCode::BAD_REQUEST, 333 - Json(json!({ 334 - "error": "InvalidCredential", 335 - "message": "Failed to parse credential response" 336 - })), 337 - ) 338 - .into_response(); 254 + return ApiError::InvalidCredential.into_response(); 339 255 } 340 256 }; 341 257 ··· 343 259 Ok(w) => w, 344 260 Err(e) => { 345 261 error!("Failed to create WebAuthn config: {:?}", e); 346 - return ( 347 - StatusCode::INTERNAL_SERVER_ERROR, 348 - Json(json!({"error": "InternalError"})), 349 - ) 350 - .into_response(); 262 + return ApiError::InternalError(None).into_response(); 351 263 } 352 264 }; 353 265 354 266 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 355 267 Ok(r) => r, 356 268 Err(e) => { 357 - warn!(did = %auth.0.did, "Passkey re-auth failed: {:?}", e); 358 - return ( 359 - StatusCode::UNAUTHORIZED, 360 - Json(json!({ 361 - "error": "AuthenticationFailed", 362 - "message": "Passkey authentication failed" 363 - })), 364 - ) 269 + warn!(did = %&auth.0.did, "Passkey re-auth failed: {:?}", e); 270 + return ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) 365 271 .into_response(); 366 272 } 367 273 }; ··· 375 281 .await 376 282 { 377 283 Ok(false) => { 378 - warn!(did = %auth.0.did, "Passkey counter anomaly detected - possible cloned key"); 284 + warn!(did = %&auth.0.did, "Passkey counter anomaly detected - possible cloned key"); 379 285 let _ = 380 286 crate::auth::webauthn::delete_authentication_state(&state.db, &auth.0.did).await; 381 - return ( 382 - StatusCode::UNAUTHORIZED, 383 - Json(json!({ 384 - "error": "PasskeyCounterAnomaly", 385 - "message": "Authentication failed: security key counter anomaly detected. This may indicate a cloned key." 386 - })), 387 - ) 388 - .into_response(); 287 + return ApiError::PasskeyCounterAnomaly.into_response(); 389 288 } 390 289 Err(e) => { 391 290 error!("Failed to update passkey counter: {:?}", e); ··· 397 296 398 297 match update_last_reauth_cached(&state.db, &state.cache, &auth.0.did).await { 399 298 Ok(reauthed_at) => { 400 - info!(did = %auth.0.did, "Re-auth successful via passkey"); 299 + info!(did = %&auth.0.did, "Re-auth successful via passkey"); 401 300 Json(ReauthResponse { reauthed_at }).into_response() 402 301 } 403 302 Err(e) => { 404 303 error!("DB error updating reauth: {:?}", e); 405 - ( 406 - StatusCode::INTERNAL_SERVER_ERROR, 407 - Json(json!({"error": "InternalError"})), 408 - ) 409 - .into_response() 304 + ApiError::InternalError(None).into_response() 410 305 } 411 306 } 412 307 } ··· 582 477 let methods = get_available_reauth_methods(db, did).await; 583 478 ( 584 479 StatusCode::FORBIDDEN, 585 - Json(serde_json::json!({ 586 - "error": "MfaVerificationRequired", 587 - "message": "This sensitive operation requires MFA verification. Your session was created via a legacy app that doesn't support MFA during login.", 588 - "reauthMethods": methods 589 - })), 480 + Json(MfaVerificationRequiredError { 481 + error: "MfaVerificationRequired".to_string(), 482 + message: "This sensitive operation requires MFA verification. Your session was created via a legacy app that doesn't support MFA during login.".to_string(), 483 + reauth_methods: methods, 484 + }), 590 485 ) 591 486 .into_response() 592 487 } 488 + 489 + #[derive(Serialize)] 490 + #[serde(rename_all = "camelCase")] 491 + pub struct MfaVerificationRequiredError { 492 + pub error: String, 493 + pub message: String, 494 + pub reauth_methods: Vec<String>, 495 + }
+30 -68
src/api/server/service_auth.rs
··· 1 - use crate::api::ApiError; 1 + use crate::types::Did; 2 + use crate::AccountStatus; 3 + use crate::api::error::ApiError; 2 4 use crate::state::AppState; 3 5 use axum::{ 4 6 Json, ··· 92 94 .await 93 95 { 94 96 Ok(result) => crate::auth::AuthenticatedUser { 95 - did: result.did, 97 + did: Did::new_unchecked(result.did), 96 98 is_oauth: true, 97 99 is_admin: false, 98 - is_takendown: false, 100 + status: AccountStatus::Active, 99 101 scope: result.scope, 100 102 key_bytes: None, 101 103 controller_did: None, ··· 113 115 } 114 116 Err(e) => { 115 117 warn!(error = ?e, "getServiceAuth DPoP auth validation failed"); 116 - return ( 117 - StatusCode::UNAUTHORIZED, 118 - Json(json!({ 119 - "error": "AuthenticationFailed", 120 - "message": format!("{:?}", e) 121 - })), 122 - ) 123 - .into_response(); 118 + return ApiError::AuthenticationFailed(Some(format!("{:?}", e))).into_response(); 124 119 } 125 120 } 126 121 } else { ··· 133 128 } 134 129 }; 135 130 info!( 136 - did = %auth_user.did, 131 + did = %&auth_user.did, 137 132 is_oauth = auth_user.is_oauth, 138 133 has_key = auth_user.key_bytes.is_some(), 139 134 "getServiceAuth auth validated" ··· 141 136 let key_bytes = match &auth_user.key_bytes { 142 137 Some(kb) => kb.clone(), 143 138 None => { 144 - warn!(did = %auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB"); 139 + warn!(did = %&auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB"); 145 140 match sqlx::query_as::<_, (Vec<u8>, Option<i32>)>( 146 141 "SELECT k.key_bytes, k.encryption_version 147 142 FROM users u ··· 157 152 Ok(key) => key, 158 153 Err(e) => { 159 154 error!(error = ?e, "Failed to decrypt user key for service auth"); 160 - return ApiError::AuthenticationFailedMsg( 155 + return ApiError::AuthenticationFailed(Some( 161 156 "Failed to get signing key".into(), 162 - ) 157 + )) 163 158 .into_response(); 164 159 } 165 160 } 166 161 } 167 162 Ok(None) => { 168 - return ApiError::AuthenticationFailedMsg("User has no signing key".into()) 163 + return ApiError::AuthenticationFailed(Some("User has no signing key".into())) 169 164 .into_response(); 170 165 } 171 166 Err(e) => { 172 167 error!(error = ?e, "DB error fetching user key"); 173 - return ApiError::AuthenticationFailedMsg("Failed to get signing key".into()) 168 + return ApiError::AuthenticationFailed(Some("Failed to get signing key".into())) 174 169 .into_response(); 175 170 } 176 171 } ··· 192 187 } else if auth_user.is_oauth { 193 188 let permissions = auth_user.permissions(); 194 189 if !permissions.has_full_access() { 195 - return ( 196 - StatusCode::BAD_REQUEST, 197 - Json(json!({ 198 - "error": "InvalidRequest", 199 - "message": "OAuth tokens with granular scopes must specify an lxm parameter" 200 - })), 190 + return ApiError::InvalidRequest( 191 + "OAuth tokens with granular scopes must specify an lxm parameter".into(), 201 192 ) 202 - .into_response(); 193 + .into_response(); 203 194 } 204 195 } 205 196 206 197 let user_status = sqlx::query!( 207 198 "SELECT takedown_ref FROM users WHERE did = $1", 208 - auth_user.did 199 + &auth_user.did 209 200 ) 210 201 .fetch_optional(&state.db) 211 202 .await; ··· 216 207 }; 217 208 218 209 if is_takendown && lxm != Some("com.atproto.server.createAccount") { 219 - return ( 220 - StatusCode::BAD_REQUEST, 221 - Json(json!({ 222 - "error": "InvalidToken", 223 - "message": "Bad token scope" 224 - })), 225 - ) 226 - .into_response(); 210 + return ApiError::InvalidToken(Some("Bad token scope".into())).into_response(); 227 211 } 228 212 229 213 if let Some(method) = lxm 230 214 && PROTECTED_METHODS.contains(&method) 231 215 { 232 - return ( 233 - StatusCode::BAD_REQUEST, 234 - Json(json!({ 235 - "error": "InvalidRequest", 236 - "message": format!("cannot request a service auth token for the following protected method: {}", method) 237 - })), 238 - ) 239 - .into_response(); 216 + return ApiError::InvalidRequest(format!( 217 + "cannot request a service auth token for the following protected method: {}", 218 + method 219 + )) 220 + .into_response(); 240 221 } 241 222 242 223 if let Some(exp) = params.exp { ··· 244 225 let diff = exp - now; 245 226 246 227 if diff < 0 { 247 - return ( 248 - StatusCode::BAD_REQUEST, 249 - Json(json!({ 250 - "error": "BadExpiration", 251 - "message": "expiration is in past" 252 - })), 253 - ) 254 - .into_response(); 228 + return ApiError::InvalidRequest("expiration is in past".into()).into_response(); 255 229 } 256 230 257 231 if diff > HOUR_SECS { 258 - return ( 259 - StatusCode::BAD_REQUEST, 260 - Json(json!({ 261 - "error": "BadExpiration", 262 - "message": "cannot request a token with an expiration more than an hour in the future" 263 - })), 232 + return ApiError::InvalidRequest( 233 + "cannot request a token with an expiration more than an hour in the future".into(), 264 234 ) 265 - .into_response(); 235 + .into_response(); 266 236 } 267 237 268 238 if lxm.is_none() && diff > MINUTE_SECS { 269 - return ( 270 - StatusCode::BAD_REQUEST, 271 - Json(json!({ 272 - "error": "BadExpiration", 273 - "message": "cannot request a method-less token with an expiration more than a minute in the future" 274 - })), 239 + return ApiError::InvalidRequest( 240 + "cannot request a method-less token with an expiration more than a minute in the future".into(), 275 241 ) 276 - .into_response(); 242 + .into_response(); 277 243 } 278 244 } 279 245 ··· 286 252 Ok(t) => t, 287 253 Err(e) => { 288 254 error!("Failed to create service token: {:?}", e); 289 - return ( 290 - StatusCode::INTERNAL_SERVER_ERROR, 291 - Json(json!({"error": "InternalError"})), 292 - ) 293 - .into_response(); 255 + return ApiError::InternalError(None).into_response(); 294 256 } 295 257 }; 296 258 (
+162 -275
src/api/server/session.rs
··· 1 - use crate::api::ApiError; 1 + use crate::api::error::ApiError; 2 + use crate::api::{EmptyResponse, SuccessResponse}; 2 3 use crate::auth::{BearerAuth, BearerAuthAllowDeactivated}; 3 4 use crate::state::{AppState, RateLimitKind}; 5 + use crate::types::{AccountState, Did, Handle, PlainPassword}; 4 6 use axum::{ 5 7 Json, 6 8 extract::State, ··· 46 48 #[serde(rename_all = "camelCase")] 47 49 pub struct CreateSessionInput { 48 50 pub identifier: String, 49 - pub password: String, 51 + pub password: PlainPassword, 50 52 #[serde(default)] 51 53 pub allow_takendown: bool, 52 54 } ··· 56 58 pub struct CreateSessionOutput { 57 59 pub access_jwt: String, 58 60 pub refresh_jwt: String, 59 - pub handle: String, 60 - pub did: String, 61 + pub handle: Handle, 62 + pub did: Did, 61 63 #[serde(skip_serializing_if = "Option::is_none")] 62 64 pub did_doc: Option<serde_json::Value>, 63 65 #[serde(skip_serializing_if = "Option::is_none")] ··· 85 87 .await 86 88 { 87 89 warn!(ip = %client_ip, "Login rate limit exceeded"); 88 - return ( 89 - StatusCode::TOO_MANY_REQUESTS, 90 - Json(json!({ 91 - "error": "RateLimitExceeded", 92 - "message": "Too many login attempts. Please try again later." 93 - })), 94 - ) 95 - .into_response(); 90 + return ApiError::RateLimitExceeded(None).into_response(); 96 91 } 97 92 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 98 93 let normalized_identifier = normalize_handle(&input.identifier, &pds_hostname); ··· 123 118 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", 124 119 ); 125 120 warn!("User not found for login attempt"); 126 - return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()) 121 + return ApiError::AuthenticationFailed(Some("Invalid identifier or password".into())) 127 122 .into_response(); 128 123 } 129 124 Err(e) => { 130 125 error!("Database error fetching user: {:?}", e); 131 - return ApiError::InternalError.into_response(); 126 + return ApiError::InternalError(None).into_response(); 132 127 } 133 128 }; 134 129 let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 135 130 Ok(k) => k, 136 131 Err(e) => { 137 132 error!("Failed to decrypt user key: {:?}", e); 138 - return ApiError::InternalError.into_response(); 133 + return ApiError::InternalError(None).into_response(); 139 134 } 140 135 }; 141 136 let (password_valid, app_password_name, app_password_scopes, app_password_controller) = if row ··· 168 163 }; 169 164 if !password_valid { 170 165 warn!("Password verification failed for login attempt"); 171 - return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()) 166 + return ApiError::AuthenticationFailed(Some("Invalid identifier or password".into())) 172 167 .into_response(); 173 168 } 174 - let is_takendown = row.takedown_ref.is_some(); 175 - if is_takendown && !input.allow_takendown { 169 + let account_state = AccountState::from_db_fields( 170 + row.deactivated_at, 171 + row.takedown_ref.clone(), 172 + row.migrated_to_pds.clone(), 173 + None, 174 + ); 175 + if account_state.is_takendown() && !input.allow_takendown { 176 176 warn!("Login attempt for takendown account: {}", row.did); 177 - return ( 178 - StatusCode::UNAUTHORIZED, 179 - Json(json!({ 180 - "error": "AccountTakedown", 181 - "message": "Account has been taken down" 182 - })), 183 - ) 184 - .into_response(); 177 + return ApiError::AccountTakedown.into_response(); 185 178 } 186 179 let is_verified = 187 180 row.email_verified || row.discord_verified || row.telegram_verified || row.signal_verified; ··· 223 216 Ok(m) => m, 224 217 Err(e) => { 225 218 error!("Failed to create access token: {:?}", e); 226 - return ApiError::InternalError.into_response(); 219 + return ApiError::InternalError(None).into_response(); 227 220 } 228 221 }; 229 222 let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { 230 223 Ok(m) => m, 231 224 Err(e) => { 232 225 error!("Failed to create refresh token: {:?}", e); 233 - return ApiError::InternalError.into_response(); 226 + return ApiError::InternalError(None).into_response(); 234 227 } 235 228 }; 236 229 let did_for_doc = row.did.clone(); ··· 254 247 ); 255 248 if let Err(e) = insert_result { 256 249 error!("Failed to insert session: {:?}", e); 257 - return ApiError::InternalError.into_response(); 250 + return ApiError::InternalError(None).into_response(); 258 251 } 259 252 if is_legacy_login { 260 253 warn!( ··· 276 269 } 277 270 } 278 271 let handle = full_handle(&row.handle, &pds_hostname); 279 - let is_migrated = row.deactivated_at.is_some() && row.migrated_to_pds.is_some(); 280 - let is_active = row.deactivated_at.is_none() && !is_takendown; 281 - let status = if is_takendown { 282 - Some("takendown".to_string()) 283 - } else if is_migrated { 284 - Some("migrated".to_string()) 285 - } else if row.deactivated_at.is_some() { 286 - Some("deactivated".to_string()) 287 - } else { 288 - None 289 - }; 272 + let is_active = account_state.is_active(); 273 + let status = account_state.status_for_session().map(String::from); 290 274 Json(CreateSessionOutput { 291 275 access_jwt: access_meta.token, 292 276 refresh_jwt: refresh_meta.token, 293 - handle, 294 - did: row.did, 277 + handle: handle.into(), 278 + did: row.did.into(), 295 279 did_doc, 296 280 email: row.email, 297 281 email_confirmed: Some(row.email_verified), ··· 317 301 preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel", 318 302 discord_verified, telegram_verified, signal_verified, migrated_to_pds, migrated_at 319 303 FROM users WHERE did = $1"#, 320 - auth_user.did 304 + &auth_user.did 321 305 ) 322 306 .fetch_optional(&state.db), 323 307 did_resolver.resolve_did_document(&did_for_doc) ··· 333 317 let pds_hostname = 334 318 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 335 319 let handle = full_handle(&row.handle, &pds_hostname); 336 - let is_takendown = row.takedown_ref.is_some(); 337 - let is_migrated = row.deactivated_at.is_some() && row.migrated_to_pds.is_some(); 338 - let is_active = row.deactivated_at.is_none() && !is_takendown; 320 + let account_state = AccountState::from_db_fields( 321 + row.deactivated_at, 322 + row.takedown_ref.clone(), 323 + row.migrated_to_pds.clone(), 324 + row.migrated_at, 325 + ); 339 326 let email_value = if can_read_email { 340 327 row.email.clone() 341 328 } else { ··· 344 331 let email_confirmed_value = can_read_email && row.email_verified; 345 332 let mut response = json!({ 346 333 "handle": handle, 347 - "did": auth_user.did, 348 - "active": is_active, 334 + "did": &auth_user.did, 335 + "active": account_state.is_active(), 349 336 "preferredChannel": preferred_channel, 350 337 "preferredChannelVerified": preferred_channel_verified, 351 338 "preferredLocale": row.preferred_locale, ··· 355 342 response["email"] = json!(email_value); 356 343 response["emailConfirmed"] = json!(email_confirmed_value); 357 344 } 358 - if is_takendown { 359 - response["status"] = json!("takendown"); 360 - } else if is_migrated { 361 - response["status"] = json!("migrated"); 362 - response["migratedToPds"] = json!(row.migrated_to_pds); 363 - response["migratedAt"] = json!(row.migrated_at); 364 - } else if row.deactivated_at.is_some() { 365 - response["status"] = json!("deactivated"); 345 + if let Some(status) = account_state.status_for_session() { 346 + response["status"] = json!(status); 347 + } 348 + if let AccountState::Migrated { to_pds, at } = &account_state { 349 + response["migratedToPds"] = json!(to_pds); 350 + response["migratedAt"] = json!(at); 366 351 } 367 352 if let Some(doc) = did_doc { 368 353 response["didDoc"] = doc; 369 354 } 370 355 Json(response).into_response() 371 356 } 372 - Ok(None) => ApiError::AuthenticationFailed.into_response(), 357 + Ok(None) => ApiError::AuthenticationFailed(None).into_response(), 373 358 Err(e) => { 374 359 error!("Database error in get_session: {:?}", e); 375 - ApiError::InternalError.into_response() 360 + ApiError::InternalError(None).into_response() 376 361 } 377 362 } 378 363 } ··· 389 374 }; 390 375 let jti = match crate::auth::get_jti_from_token(&token) { 391 376 Ok(jti) => jti, 392 - Err(_) => return ApiError::AuthenticationFailed.into_response(), 377 + Err(_) => return ApiError::AuthenticationFailed(None).into_response(), 393 378 }; 394 379 let did = crate::auth::get_did_from_token(&token).ok(); 395 380 match sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti) ··· 401 386 let session_cache_key = format!("auth:session:{}:{}", did, jti); 402 387 let _ = state.cache.delete(&session_cache_key).await; 403 388 } 404 - Json(json!({})).into_response() 389 + EmptyResponse::ok().into_response() 405 390 } 406 - Ok(_) => ApiError::AuthenticationFailed.into_response(), 391 + Ok(_) => ApiError::AuthenticationFailed(None).into_response(), 407 392 Err(e) => { 408 393 error!("Database error in delete_session: {:?}", e); 409 - ApiError::AuthenticationFailed.into_response() 394 + ApiError::AuthenticationFailed(None).into_response() 410 395 } 411 396 } 412 397 } ··· 421 406 .await 422 407 { 423 408 tracing::warn!(ip = %client_ip, "Refresh session rate limit exceeded"); 424 - return ( 425 - axum::http::StatusCode::TOO_MANY_REQUESTS, 426 - axum::Json(serde_json::json!({ 427 - "error": "RateLimitExceeded", 428 - "message": "Too many requests. Please try again later." 429 - })), 430 - ) 431 - .into_response(); 409 + return ApiError::RateLimitExceeded(None).into_response(); 432 410 } 433 411 let refresh_token = match crate::auth::extract_bearer_token_from_header( 434 412 headers.get("Authorization").and_then(|h| h.to_str().ok()), ··· 439 417 let refresh_jti = match crate::auth::get_jti_from_token(&refresh_token) { 440 418 Ok(jti) => jti, 441 419 Err(_) => { 442 - return ApiError::AuthenticationFailedMsg("Invalid token format".into()) 420 + return ApiError::AuthenticationFailed(Some("Invalid token format".into())) 443 421 .into_response(); 444 422 } 445 423 }; ··· 447 425 Ok(tx) => tx, 448 426 Err(e) => { 449 427 error!("Failed to begin transaction: {:?}", e); 450 - return ApiError::InternalError.into_response(); 428 + return ApiError::InternalError(None).into_response(); 451 429 } 452 430 }; 453 431 if let Ok(Some(session_id)) = sqlx::query_scalar!( ··· 465 443 .execute(&mut *tx) 466 444 .await; 467 445 let _ = tx.commit().await; 468 - return ApiError::ExpiredTokenMsg( 446 + return ApiError::AuthenticationFailed(Some( 469 447 "Refresh token has been revoked due to suspected compromise".into(), 470 - ) 448 + )) 471 449 .into_response(); 472 450 } 473 451 let session_row = match sqlx::query!( ··· 484 462 { 485 463 Ok(Some(row)) => row, 486 464 Ok(None) => { 487 - return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()) 465 + return ApiError::AuthenticationFailed(Some("Invalid refresh token".into())) 488 466 .into_response(); 489 467 } 490 468 Err(e) => { 491 469 error!("Database error fetching session: {:?}", e); 492 - return ApiError::InternalError.into_response(); 470 + return ApiError::InternalError(None).into_response(); 493 471 } 494 472 }; 495 473 let key_bytes = ··· 497 475 Ok(k) => k, 498 476 Err(e) => { 499 477 error!("Failed to decrypt user key: {:?}", e); 500 - return ApiError::InternalError.into_response(); 478 + return ApiError::InternalError(None).into_response(); 501 479 } 502 480 }; 503 481 if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { 504 - return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response(); 482 + return ApiError::AuthenticationFailed(Some("Invalid refresh token".into())).into_response(); 505 483 } 506 484 let new_access_meta = match crate::auth::create_access_token_with_delegation( 507 485 &session_row.did, ··· 512 490 Ok(m) => m, 513 491 Err(e) => { 514 492 error!("Failed to create access token: {:?}", e); 515 - return ApiError::InternalError.into_response(); 493 + return ApiError::InternalError(None).into_response(); 516 494 } 517 495 }; 518 496 let new_refresh_meta = ··· 520 498 Ok(m) => m, 521 499 Err(e) => { 522 500 error!("Failed to create refresh token: {:?}", e); 523 - return ApiError::InternalError.into_response(); 501 + return ApiError::InternalError(None).into_response(); 524 502 } 525 503 }; 526 504 match sqlx::query!( ··· 537 515 .execute(&mut *tx) 538 516 .await; 539 517 let _ = tx.commit().await; 540 - return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response(); 518 + return ApiError::AuthenticationFailed(Some("Refresh token has been revoked due to suspected compromise".into())).into_response(); 541 519 } 542 520 Err(e) => { 543 521 error!("Failed to record used refresh token: {:?}", e); 544 - return ApiError::InternalError.into_response(); 522 + return ApiError::InternalError(None).into_response(); 545 523 } 546 524 Ok(_) => {} 547 525 } ··· 557 535 .await 558 536 { 559 537 error!("Database error updating session: {:?}", e); 560 - return ApiError::InternalError.into_response(); 538 + return ApiError::InternalError(None).into_response(); 561 539 } 562 540 if let Err(e) = tx.commit().await { 563 541 error!("Failed to commit transaction: {:?}", e); 564 - return ApiError::InternalError.into_response(); 542 + return ApiError::InternalError(None).into_response(); 565 543 } 566 544 let did_for_doc = session_row.did.clone(); 567 545 let did_resolver = state.did_resolver.clone(); ··· 588 566 let pds_hostname = 589 567 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 590 568 let handle = full_handle(&u.handle, &pds_hostname); 591 - let is_takendown = u.takedown_ref.is_some(); 592 - let is_active = u.deactivated_at.is_none() && !is_takendown; 569 + let account_state = AccountState::from_db_fields( 570 + u.deactivated_at, 571 + u.takedown_ref.clone(), 572 + None, 573 + None, 574 + ); 593 575 let mut response = json!({ 594 576 "accessJwt": new_access_meta.token, 595 577 "refreshJwt": new_refresh_meta.token, ··· 601 583 "preferredChannelVerified": preferred_channel_verified, 602 584 "preferredLocale": u.preferred_locale, 603 585 "isAdmin": u.is_admin, 604 - "active": is_active 586 + "active": account_state.is_active() 605 587 }); 606 588 if let Some(doc) = did_doc { 607 589 response["didDoc"] = doc; 608 590 } 609 - if is_takendown { 610 - response["status"] = json!("takendown"); 611 - } else if u.deactivated_at.is_some() { 612 - response["status"] = json!("deactivated"); 591 + if let Some(status) = account_state.status_for_session() { 592 + response["status"] = json!(status); 613 593 } 614 594 Json(response).into_response() 615 595 } 616 596 Ok(None) => { 617 597 error!("User not found for existing session: {}", session_row.did); 618 - ApiError::InternalError.into_response() 598 + ApiError::InternalError(None).into_response() 619 599 } 620 600 Err(e) => { 621 601 error!("Database error fetching user: {:?}", e); 622 - ApiError::InternalError.into_response() 602 + ApiError::InternalError(None).into_response() 623 603 } 624 604 } 625 605 } ··· 627 607 #[derive(Deserialize)] 628 608 #[serde(rename_all = "camelCase")] 629 609 pub struct ConfirmSignupInput { 630 - pub did: String, 610 + pub did: Did, 631 611 pub verification_code: String, 632 612 } 633 613 ··· 636 616 pub struct ConfirmSignupOutput { 637 617 pub access_jwt: String, 638 618 pub refresh_jwt: String, 639 - pub handle: String, 640 - pub did: String, 619 + pub handle: Handle, 620 + pub did: Did, 641 621 pub email: Option<String>, 642 622 pub email_verified: bool, 643 623 pub preferred_channel: String, ··· 658 638 FROM users u 659 639 JOIN user_keys k ON u.id = k.user_id 660 640 WHERE u.did = $1"#, 661 - input.did 641 + input.did.as_str() 662 642 ) 663 643 .fetch_optional(&state.db) 664 644 .await ··· 671 651 } 672 652 Err(e) => { 673 653 error!("Database error in confirm_signup: {:?}", e); 674 - return ApiError::InternalError.into_response(); 654 + return ApiError::InternalError(None).into_response(); 675 655 } 676 656 }; 677 657 ··· 697 677 &identifier, 698 678 ) { 699 679 Ok(token_data) => { 700 - if token_data.did != input.did { 680 + if token_data.did != input.did.as_str() { 701 681 warn!( 702 682 "Token DID mismatch for confirm_signup: expected {}, got {}", 703 683 input.did, token_data.did ··· 708 688 } 709 689 Err(crate::auth::verification_token::VerifyError::Expired) => { 710 690 warn!("Verification code expired for user: {}", input.did); 711 - return ApiError::ExpiredTokenMsg("Verification code has expired".into()) 691 + return ApiError::ExpiredToken(Some("Verification code has expired".into())) 712 692 .into_response(); 713 693 } 714 694 Err(e) => { ··· 721 701 Ok(k) => k, 722 702 Err(e) => { 723 703 error!("Failed to decrypt user key: {:?}", e); 724 - return ApiError::InternalError.into_response(); 704 + return ApiError::InternalError(None).into_response(); 725 705 } 726 706 }; 727 707 let verified_column = match row.channel { ··· 732 712 }; 733 713 let update_query = format!("UPDATE users SET {} = TRUE WHERE did = $1", verified_column); 734 714 if let Err(e) = sqlx::query(&update_query) 735 - .bind(&input.did) 715 + .bind(input.did.as_str()) 736 716 .execute(&state.db) 737 717 .await 738 718 { 739 719 error!("Failed to update verification status: {:?}", e); 740 - return ApiError::InternalError.into_response(); 720 + return ApiError::InternalError(None).into_response(); 741 721 } 742 722 743 723 let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) { 744 724 Ok(m) => m, 745 725 Err(e) => { 746 726 error!("Failed to create access token: {:?}", e); 747 - return ApiError::InternalError.into_response(); 727 + return ApiError::InternalError(None).into_response(); 748 728 } 749 729 }; 750 730 let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { 751 731 Ok(m) => m, 752 732 Err(e) => { 753 733 error!("Failed to create refresh token: {:?}", e); 754 - return ApiError::InternalError.into_response(); 734 + return ApiError::InternalError(None).into_response(); 755 735 } 756 736 }; 757 737 let no_scope: Option<String> = None; ··· 770 750 .await 771 751 { 772 752 error!("Failed to insert session: {:?}", e); 773 - return ApiError::InternalError.into_response(); 753 + return ApiError::InternalError(None).into_response(); 774 754 } 775 755 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 776 756 if let Err(e) = crate::comms::enqueue_welcome(&state.db, row.id, &hostname).await { ··· 786 766 Json(ConfirmSignupOutput { 787 767 access_jwt: access_meta.token, 788 768 refresh_jwt: refresh_meta.token, 789 - handle: row.handle, 790 - did: row.did, 769 + handle: row.handle.into(), 770 + did: row.did.into(), 791 771 email: row.email, 792 772 email_verified, 793 773 preferred_channel: preferred_channel.to_string(), ··· 799 779 #[derive(Deserialize)] 800 780 #[serde(rename_all = "camelCase")] 801 781 pub struct ResendVerificationInput { 802 - pub did: String, 782 + pub did: Did, 803 783 } 804 784 805 785 pub async fn resend_verification( ··· 815 795 email_verified, discord_verified, telegram_verified, signal_verified 816 796 FROM users 817 797 WHERE did = $1"#, 818 - input.did 798 + input.did.as_str() 819 799 ) 820 800 .fetch_optional(&state.db) 821 801 .await ··· 826 806 } 827 807 Err(e) => { 828 808 error!("Database error in resend_verification: {:?}", e); 829 - return ApiError::InternalError.into_response(); 809 + return ApiError::InternalError(None).into_response(); 830 810 } 831 811 }; 832 812 let is_verified = ··· 866 846 { 867 847 warn!("Failed to enqueue verification notification: {:?}", e); 868 848 } 869 - Json(json!({"success": true})).into_response() 849 + SuccessResponse::ok().into_response() 870 850 } 871 851 872 852 #[derive(Serialize)] ··· 934 914 } 935 915 Err(e) => { 936 916 error!("DB error fetching JWT sessions: {:?}", e); 937 - return ( 938 - StatusCode::INTERNAL_SERVER_ERROR, 939 - Json(json!({"error": "InternalError"})), 940 - ) 941 - .into_response(); 917 + return ApiError::InternalError(None).into_response(); 942 918 } 943 919 } 944 920 ··· 980 956 } 981 957 Err(e) => { 982 958 error!("DB error fetching OAuth sessions: {:?}", e); 983 - return ( 984 - StatusCode::INTERNAL_SERVER_ERROR, 985 - Json(json!({"error": "InternalError"})), 986 - ) 987 - .into_response(); 959 + return ApiError::InternalError(None).into_response(); 988 960 } 989 961 } 990 962 ··· 1015 987 Json(input): Json<RevokeSessionInput>, 1016 988 ) -> Response { 1017 989 if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { 1018 - let session_id: i32 = match jwt_id.parse() { 1019 - Ok(id) => id, 1020 - Err(_) => { 1021 - return ( 1022 - StatusCode::BAD_REQUEST, 1023 - Json(json!({"error": "InvalidRequest", "message": "Invalid session ID"})), 1024 - ) 1025 - .into_response(); 1026 - } 990 + let Ok(session_id) = jwt_id.parse::<i32>() else { 991 + return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); 1027 992 }; 1028 993 let session = sqlx::query_as::<_, (String,)>( 1029 994 "SELECT access_jti FROM session_tokens WHERE id = $1 AND did = $2", ··· 1035 1000 let access_jti = match session { 1036 1001 Ok(Some((jti,))) => jti, 1037 1002 Ok(None) => { 1038 - return ( 1039 - StatusCode::NOT_FOUND, 1040 - Json(json!({"error": "SessionNotFound", "message": "Session not found"})), 1041 - ) 1042 - .into_response(); 1003 + return ApiError::SessionNotFound.into_response(); 1043 1004 } 1044 1005 Err(e) => { 1045 1006 error!("DB error in revoke_session: {:?}", e); 1046 - return ( 1047 - StatusCode::INTERNAL_SERVER_ERROR, 1048 - Json(json!({"error": "InternalError"})), 1049 - ) 1050 - .into_response(); 1007 + return ApiError::InternalError(None).into_response(); 1051 1008 } 1052 1009 }; 1053 1010 if let Err(e) = sqlx::query("DELETE FROM session_tokens WHERE id = $1") ··· 1056 1013 .await 1057 1014 { 1058 1015 error!("DB error deleting session: {:?}", e); 1059 - return ( 1060 - StatusCode::INTERNAL_SERVER_ERROR, 1061 - Json(json!({"error": "InternalError"})), 1062 - ) 1063 - .into_response(); 1016 + return ApiError::InternalError(None).into_response(); 1064 1017 } 1065 - let cache_key = format!("auth:session:{}:{}", auth.0.did, access_jti); 1018 + let cache_key = format!("auth:session:{}:{}", &auth.0.did, access_jti); 1066 1019 if let Err(e) = state.cache.delete(&cache_key).await { 1067 1020 warn!("Failed to invalidate session cache: {:?}", e); 1068 1021 } 1069 - info!(did = %auth.0.did, session_id = %session_id, "JWT session revoked"); 1022 + info!(did = %&auth.0.did, session_id = %session_id, "JWT session revoked"); 1070 1023 } else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") { 1071 - let session_id: i32 = match oauth_id.parse() { 1072 - Ok(id) => id, 1073 - Err(_) => { 1074 - return ( 1075 - StatusCode::BAD_REQUEST, 1076 - Json(json!({"error": "InvalidRequest", "message": "Invalid session ID"})), 1077 - ) 1078 - .into_response(); 1079 - } 1024 + let Ok(session_id) = oauth_id.parse::<i32>() else { 1025 + return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); 1080 1026 }; 1081 1027 let result = sqlx::query("DELETE FROM oauth_token WHERE id = $1 AND did = $2") 1082 1028 .bind(session_id) ··· 1085 1031 .await; 1086 1032 match result { 1087 1033 Ok(r) if r.rows_affected() == 0 => { 1088 - return ( 1089 - StatusCode::NOT_FOUND, 1090 - Json(json!({"error": "SessionNotFound", "message": "Session not found"})), 1091 - ) 1092 - .into_response(); 1034 + return ApiError::SessionNotFound.into_response(); 1093 1035 } 1094 1036 Err(e) => { 1095 1037 error!("DB error deleting OAuth session: {:?}", e); 1096 - return ( 1097 - StatusCode::INTERNAL_SERVER_ERROR, 1098 - Json(json!({"error": "InternalError"})), 1099 - ) 1100 - .into_response(); 1038 + return ApiError::InternalError(None).into_response(); 1101 1039 } 1102 1040 _ => {} 1103 1041 } 1104 - info!(did = %auth.0.did, session_id = %session_id, "OAuth session revoked"); 1042 + info!(did = %&auth.0.did, session_id = %session_id, "OAuth session revoked"); 1105 1043 } else { 1106 - return ( 1107 - StatusCode::BAD_REQUEST, 1108 - Json(json!({"error": "InvalidRequest", "message": "Invalid session ID format"})), 1109 - ) 1110 - .into_response(); 1044 + return ApiError::InvalidRequest("Invalid session ID format".into()).into_response(); 1111 1045 } 1112 - (StatusCode::OK, Json(json!({}))).into_response() 1046 + EmptyResponse::ok().into_response() 1113 1047 } 1114 1048 1115 1049 pub async fn revoke_all_sessions( ··· 1123 1057 .and_then(|v| v.strip_prefix("Bearer ")) 1124 1058 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 1125 1059 1126 - if let Some(ref jti) = current_jti { 1127 - if auth.0.is_oauth { 1128 - if let Err(e) = sqlx::query("DELETE FROM session_tokens WHERE did = $1") 1129 - .bind(&auth.0.did) 1130 - .execute(&state.db) 1131 - .await 1132 - { 1133 - error!("DB error revoking JWT sessions: {:?}", e); 1134 - return ( 1135 - StatusCode::INTERNAL_SERVER_ERROR, 1136 - Json(json!({"error": "InternalError"})), 1137 - ) 1138 - .into_response(); 1139 - } 1140 - if let Err(e) = sqlx::query("DELETE FROM oauth_token WHERE did = $1 AND token_id != $2") 1060 + let Some(ref jti) = current_jti else { 1061 + return ApiError::InvalidToken(None).into_response(); 1062 + }; 1063 + 1064 + if auth.0.is_oauth { 1065 + if let Err(e) = sqlx::query("DELETE FROM session_tokens WHERE did = $1") 1066 + .bind(&auth.0.did) 1067 + .execute(&state.db) 1068 + .await 1069 + { 1070 + error!("DB error revoking JWT sessions: {:?}", e); 1071 + return ApiError::InternalError(None).into_response(); 1072 + } 1073 + if let Err(e) = sqlx::query("DELETE FROM oauth_token WHERE did = $1 AND token_id != $2") 1074 + .bind(&auth.0.did) 1075 + .bind(jti) 1076 + .execute(&state.db) 1077 + .await 1078 + { 1079 + error!("DB error revoking OAuth sessions: {:?}", e); 1080 + return ApiError::InternalError(None).into_response(); 1081 + } 1082 + } else { 1083 + if let Err(e) = 1084 + sqlx::query("DELETE FROM session_tokens WHERE did = $1 AND access_jti != $2") 1141 1085 .bind(&auth.0.did) 1142 1086 .bind(jti) 1143 1087 .execute(&state.db) 1144 1088 .await 1145 - { 1146 - error!("DB error revoking OAuth sessions: {:?}", e); 1147 - return ( 1148 - StatusCode::INTERNAL_SERVER_ERROR, 1149 - Json(json!({"error": "InternalError"})), 1150 - ) 1151 - .into_response(); 1152 - } 1153 - } else { 1154 - if let Err(e) = 1155 - sqlx::query("DELETE FROM session_tokens WHERE did = $1 AND access_jti != $2") 1156 - .bind(&auth.0.did) 1157 - .bind(jti) 1158 - .execute(&state.db) 1159 - .await 1160 - { 1161 - error!("DB error revoking JWT sessions: {:?}", e); 1162 - return ( 1163 - StatusCode::INTERNAL_SERVER_ERROR, 1164 - Json(json!({"error": "InternalError"})), 1165 - ) 1166 - .into_response(); 1167 - } 1168 - if let Err(e) = sqlx::query("DELETE FROM oauth_token WHERE did = $1") 1169 - .bind(&auth.0.did) 1170 - .execute(&state.db) 1171 - .await 1172 - { 1173 - error!("DB error revoking OAuth sessions: {:?}", e); 1174 - return ( 1175 - StatusCode::INTERNAL_SERVER_ERROR, 1176 - Json(json!({"error": "InternalError"})), 1177 - ) 1178 - .into_response(); 1179 - } 1089 + { 1090 + error!("DB error revoking JWT sessions: {:?}", e); 1091 + return ApiError::InternalError(None).into_response(); 1092 + } 1093 + if let Err(e) = sqlx::query("DELETE FROM oauth_token WHERE did = $1") 1094 + .bind(&auth.0.did) 1095 + .execute(&state.db) 1096 + .await 1097 + { 1098 + error!("DB error revoking OAuth sessions: {:?}", e); 1099 + return ApiError::InternalError(None).into_response(); 1180 1100 } 1181 - } else { 1182 - return ( 1183 - StatusCode::BAD_REQUEST, 1184 - Json(json!({"error": "InvalidToken", "message": "Could not identify current session"})), 1185 - ) 1186 - .into_response(); 1187 1101 } 1188 1102 1189 - info!(did = %auth.0.did, "All other sessions revoked"); 1190 - (StatusCode::OK, Json(json!({"success": true}))).into_response() 1103 + info!(did = %&auth.0.did, "All other sessions revoked"); 1104 + SuccessResponse::ok().into_response() 1191 1105 } 1192 1106 1193 1107 #[derive(Serialize)] ··· 1207 1121 (EXISTS(SELECT 1 FROM user_totp t WHERE t.did = u.did AND t.verified = TRUE) OR 1208 1122 EXISTS(SELECT 1 FROM passkeys p WHERE p.did = u.did)) as "has_mfa!" 1209 1123 FROM users u WHERE u.did = $1"#, 1210 - auth.0.did 1124 + &auth.0.did 1211 1125 ) 1212 1126 .fetch_optional(&state.db) 1213 1127 .await; ··· 1218 1132 has_mfa: row.has_mfa, 1219 1133 }) 1220 1134 .into_response(), 1221 - Ok(None) => ( 1222 - StatusCode::NOT_FOUND, 1223 - Json(json!({"error": "AccountNotFound"})), 1224 - ) 1225 - .into_response(), 1135 + Ok(None) => ApiError::AccountNotFound.into_response(), 1226 1136 Err(e) => { 1227 1137 error!("DB error: {:?}", e); 1228 - ( 1229 - StatusCode::INTERNAL_SERVER_ERROR, 1230 - Json(json!({"error": "InternalError"})), 1231 - ) 1232 - .into_response() 1138 + ApiError::InternalError(None).into_response() 1233 1139 } 1234 1140 } 1235 1141 } ··· 1257 1163 let result = sqlx::query!( 1258 1164 "UPDATE users SET allow_legacy_login = $1 WHERE did = $2 RETURNING did", 1259 1165 input.allow_legacy_login, 1260 - auth.0.did 1166 + &auth.0.did 1261 1167 ) 1262 1168 .fetch_optional(&state.db) 1263 1169 .await; ··· 1265 1171 match result { 1266 1172 Ok(Some(_)) => { 1267 1173 info!( 1268 - did = %auth.0.did, 1174 + did = %&auth.0.did, 1269 1175 allow_legacy_login = input.allow_legacy_login, 1270 1176 "Legacy login preference updated" 1271 1177 ); ··· 1274 1180 })) 1275 1181 .into_response() 1276 1182 } 1277 - Ok(None) => ( 1278 - StatusCode::NOT_FOUND, 1279 - Json(json!({"error": "AccountNotFound"})), 1280 - ) 1281 - .into_response(), 1183 + Ok(None) => ApiError::AccountNotFound.into_response(), 1282 1184 Err(e) => { 1283 1185 error!("DB error: {:?}", e); 1284 - ( 1285 - StatusCode::INTERNAL_SERVER_ERROR, 1286 - Json(json!({"error": "InternalError"})), 1287 - ) 1288 - .into_response() 1186 + ApiError::InternalError(None).into_response() 1289 1187 } 1290 1188 } 1291 1189 } ··· 1304 1202 Json(input): Json<UpdateLocaleInput>, 1305 1203 ) -> Response { 1306 1204 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1307 - return ( 1308 - StatusCode::BAD_REQUEST, 1309 - Json(json!({ 1310 - "error": "InvalidRequest", 1311 - "message": format!("Invalid locale. Valid options: {}", VALID_LOCALES.join(", ")) 1312 - })), 1313 - ) 1314 - .into_response(); 1205 + return ApiError::InvalidRequest(format!( 1206 + "Invalid locale. Valid options: {}", 1207 + VALID_LOCALES.join(", ") 1208 + )) 1209 + .into_response(); 1315 1210 } 1316 1211 1317 1212 let result = sqlx::query!( 1318 1213 "UPDATE users SET preferred_locale = $1 WHERE did = $2 RETURNING did", 1319 1214 input.preferred_locale, 1320 - auth.0.did 1215 + &auth.0.did 1321 1216 ) 1322 1217 .fetch_optional(&state.db) 1323 1218 .await; ··· 1325 1220 match result { 1326 1221 Ok(Some(_)) => { 1327 1222 info!( 1328 - did = %auth.0.did, 1223 + did = %&auth.0.did, 1329 1224 locale = %input.preferred_locale, 1330 1225 "User locale preference updated" 1331 1226 ); ··· 1334 1229 })) 1335 1230 .into_response() 1336 1231 } 1337 - Ok(None) => ( 1338 - StatusCode::NOT_FOUND, 1339 - Json(json!({"error": "AccountNotFound"})), 1340 - ) 1341 - .into_response(), 1232 + Ok(None) => ApiError::AccountNotFound.into_response(), 1342 1233 Err(e) => { 1343 1234 error!("DB error updating locale: {:?}", e); 1344 - ( 1345 - StatusCode::INTERNAL_SERVER_ERROR, 1346 - Json(json!({"error": "InternalError"})), 1347 - ) 1348 - .into_response() 1235 + ApiError::InternalError(None).into_response() 1349 1236 } 1350 1237 } 1351 1238 }
+2 -6
src/api/server/signing_key.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 8 9 use chrono::{Duration, Utc}; 9 10 use k256::ecdsa::SigningKey; 10 11 use serde::{Deserialize, Serialize}; 11 - use serde_json::json; 12 12 use tracing::{error, info}; 13 13 14 14 const SECP256K1_MULTICODEC_PREFIX: [u8; 2] = [0xe7, 0x01]; ··· 69 69 } 70 70 Err(e) => { 71 71 error!("DB error in reserve_signing_key: {:?}", e); 72 - ( 73 - StatusCode::INTERNAL_SERVER_ERROR, 74 - Json(json!({"error": "InternalError"})), 75 - ) 76 - .into_response() 72 + ApiError::InternalError(None).into_response() 77 73 } 78 74 } 79 75 }
+74 -300
src/api/server/totp.rs
··· 1 + use crate::api::EmptyResponse; 2 + use crate::api::error::ApiError; 1 3 use crate::auth::BearerAuth; 2 4 use crate::auth::totp::{ 3 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, ··· 5 7 verify_backup_code, verify_totp_code, 6 8 }; 7 9 use crate::state::{AppState, RateLimitKind}; 10 + use crate::types::PlainPassword; 8 11 use axum::{ 9 12 Json, 10 13 extract::State, 11 - http::StatusCode, 12 14 response::{IntoResponse, Response}, 13 15 }; 14 16 use chrono::Utc; 15 17 use serde::{Deserialize, Serialize}; 16 - use serde_json::json; 17 18 use tracing::{error, info, warn}; 18 19 19 20 const ENCRYPTION_VERSION: i32 = 1; ··· 27 28 } 28 29 29 30 pub async fn create_totp_secret(State(state): State<AppState>, auth: BearerAuth) -> Response { 30 - let existing = sqlx::query_scalar!("SELECT verified FROM user_totp WHERE did = $1", auth.0.did) 31 + let existing = sqlx::query_scalar!("SELECT verified FROM user_totp WHERE did = $1", &*&auth.0.did) 31 32 .fetch_optional(&state.db) 32 33 .await; 33 34 34 35 if let Ok(Some(true)) = existing { 35 - return ( 36 - StatusCode::CONFLICT, 37 - Json(json!({ 38 - "error": "TotpAlreadyEnabled", 39 - "message": "TOTP is already enabled for this account" 40 - })), 41 - ) 42 - .into_response(); 36 + return ApiError::TotpAlreadyEnabled.into_response(); 43 37 } 44 38 45 39 let secret = generate_totp_secret(); 46 40 47 - let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", auth.0.did) 41 + let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", &*&auth.0.did) 48 42 .fetch_optional(&state.db) 49 43 .await; 50 44 51 45 let handle = match handle { 52 46 Ok(Some(h)) => h, 53 - Ok(None) => { 54 - return ( 55 - StatusCode::NOT_FOUND, 56 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 57 - ) 58 - .into_response(); 59 - } 47 + Ok(None) => return ApiError::AccountNotFound.into_response(), 60 48 Err(e) => { 61 49 error!("DB error fetching handle: {:?}", e); 62 - return ( 63 - StatusCode::INTERNAL_SERVER_ERROR, 64 - Json(json!({"error": "InternalError"})), 65 - ) 66 - .into_response(); 50 + return ApiError::InternalError(None).into_response(); 67 51 } 68 52 }; 69 53 ··· 74 58 Ok(qr) => qr, 75 59 Err(e) => { 76 60 error!("Failed to generate QR code: {:?}", e); 77 - return ( 78 - StatusCode::INTERNAL_SERVER_ERROR, 79 - Json(json!({"error": "InternalError", "message": "Failed to generate QR code"})), 80 - ) 81 - .into_response(); 61 + return ApiError::InternalError(Some("Failed to generate QR code".into())).into_response(); 82 62 } 83 63 }; 84 64 ··· 86 66 Ok(enc) => enc, 87 67 Err(e) => { 88 68 error!("Failed to encrypt TOTP secret: {:?}", e); 89 - return ( 90 - StatusCode::INTERNAL_SERVER_ERROR, 91 - Json(json!({"error": "InternalError"})), 92 - ) 93 - .into_response(); 69 + return ApiError::InternalError(None).into_response(); 94 70 } 95 71 }; 96 72 ··· 105 81 created_at = NOW(), 106 82 last_used = NULL 107 83 "#, 108 - auth.0.did, 84 + &auth.0.did, 109 85 encrypted_secret, 110 86 ENCRYPTION_VERSION 111 87 ) ··· 114 90 115 91 if let Err(e) = result { 116 92 error!("Failed to store TOTP secret: {:?}", e); 117 - return ( 118 - StatusCode::INTERNAL_SERVER_ERROR, 119 - Json(json!({"error": "InternalError"})), 120 - ) 121 - .into_response(); 93 + return ApiError::InternalError(None).into_response(); 122 94 } 123 95 124 96 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 125 97 126 - info!(did = %auth.0.did, "TOTP secret created (pending verification)"); 98 + info!(did = %&auth.0.did, "TOTP secret created (pending verification)"); 127 99 128 100 Json(CreateTotpSecretResponse { 129 101 secret: secret_base32, ··· 153 125 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 154 126 .await 155 127 { 156 - warn!(did = %auth.0.did, "TOTP verification rate limit exceeded"); 157 - return ( 158 - StatusCode::TOO_MANY_REQUESTS, 159 - Json(json!({ 160 - "error": "RateLimitExceeded", 161 - "message": "Too many verification attempts. Please try again in a few minutes." 162 - })), 163 - ) 164 - .into_response(); 128 + warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 129 + return ApiError::RateLimitExceeded(None).into_response(); 165 130 } 166 131 167 132 let totp_row = sqlx::query!( 168 133 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 169 - auth.0.did 134 + &auth.0.did 170 135 ) 171 136 .fetch_optional(&state.db) 172 137 .await; 173 138 174 139 let totp_row = match totp_row { 175 140 Ok(Some(row)) => row, 176 - Ok(None) => { 177 - return ( 178 - StatusCode::BAD_REQUEST, 179 - Json(json!({ 180 - "error": "TotpNotSetup", 181 - "message": "Please call createTotpSecret first" 182 - })), 183 - ) 184 - .into_response(); 185 - } 141 + Ok(None) => return ApiError::TotpNotEnabled.into_response(), 186 142 Err(e) => { 187 143 error!("DB error fetching TOTP: {:?}", e); 188 - return ( 189 - StatusCode::INTERNAL_SERVER_ERROR, 190 - Json(json!({"error": "InternalError"})), 191 - ) 192 - .into_response(); 144 + return ApiError::InternalError(None).into_response(); 193 145 } 194 146 }; 195 147 196 148 if totp_row.verified { 197 - return ( 198 - StatusCode::CONFLICT, 199 - Json(json!({ 200 - "error": "TotpAlreadyEnabled", 201 - "message": "TOTP is already enabled" 202 - })), 203 - ) 204 - .into_response(); 149 + return ApiError::TotpAlreadyEnabled.into_response(); 205 150 } 206 151 207 152 let secret = match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version) ··· 209 154 Ok(s) => s, 210 155 Err(e) => { 211 156 error!("Failed to decrypt TOTP secret: {:?}", e); 212 - return ( 213 - StatusCode::INTERNAL_SERVER_ERROR, 214 - Json(json!({"error": "InternalError"})), 215 - ) 216 - .into_response(); 157 + return ApiError::InternalError(None).into_response(); 217 158 } 218 159 }; 219 160 220 161 let code = input.code.trim(); 221 162 if !verify_totp_code(&secret, code) { 222 - return ( 223 - StatusCode::UNAUTHORIZED, 224 - Json(json!({ 225 - "error": "InvalidCode", 226 - "message": "Invalid verification code" 227 - })), 228 - ) 229 - .into_response(); 163 + return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 230 164 } 231 165 232 166 let backup_codes = generate_backup_codes(); ··· 234 168 Ok(tx) => tx, 235 169 Err(e) => { 236 170 error!("Failed to begin transaction: {:?}", e); 237 - return ( 238 - StatusCode::INTERNAL_SERVER_ERROR, 239 - Json(json!({"error": "InternalError"})), 240 - ) 241 - .into_response(); 171 + return ApiError::InternalError(None).into_response(); 242 172 } 243 173 }; 244 174 245 175 if let Err(e) = sqlx::query!( 246 176 "UPDATE user_totp SET verified = true, last_used = NOW() WHERE did = $1", 247 - auth.0.did 177 + &auth.0.did 248 178 ) 249 179 .execute(&mut *tx) 250 180 .await 251 181 { 252 182 error!("Failed to enable TOTP: {:?}", e); 253 - return ( 254 - StatusCode::INTERNAL_SERVER_ERROR, 255 - Json(json!({"error": "InternalError"})), 256 - ) 257 - .into_response(); 183 + return ApiError::InternalError(None).into_response(); 258 184 } 259 185 260 - if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", auth.0.did) 186 + if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", &*&auth.0.did) 261 187 .execute(&mut *tx) 262 188 .await 263 189 { 264 190 error!("Failed to clear old backup codes: {:?}", e); 265 - return ( 266 - StatusCode::INTERNAL_SERVER_ERROR, 267 - Json(json!({"error": "InternalError"})), 268 - ) 269 - .into_response(); 191 + return ApiError::InternalError(None).into_response(); 270 192 } 271 193 272 194 for code in &backup_codes { ··· 274 196 Ok(h) => h, 275 197 Err(e) => { 276 198 error!("Failed to hash backup code: {:?}", e); 277 - return ( 278 - StatusCode::INTERNAL_SERVER_ERROR, 279 - Json(json!({"error": "InternalError"})), 280 - ) 281 - .into_response(); 199 + return ApiError::InternalError(None).into_response(); 282 200 } 283 201 }; 284 202 285 203 if let Err(e) = sqlx::query!( 286 204 "INSERT INTO backup_codes (did, code_hash, created_at) VALUES ($1, $2, NOW())", 287 - auth.0.did, 205 + &auth.0.did, 288 206 hash 289 207 ) 290 208 .execute(&mut *tx) 291 209 .await 292 210 { 293 211 error!("Failed to store backup code: {:?}", e); 294 - return ( 295 - StatusCode::INTERNAL_SERVER_ERROR, 296 - Json(json!({"error": "InternalError"})), 297 - ) 298 - .into_response(); 212 + return ApiError::InternalError(None).into_response(); 299 213 } 300 214 } 301 215 302 216 if let Err(e) = tx.commit().await { 303 217 error!("Failed to commit transaction: {:?}", e); 304 - return ( 305 - StatusCode::INTERNAL_SERVER_ERROR, 306 - Json(json!({"error": "InternalError"})), 307 - ) 308 - .into_response(); 218 + return ApiError::InternalError(None).into_response(); 309 219 } 310 220 311 - info!(did = %auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); 221 + info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); 312 222 313 223 Json(EnableTotpResponse { backup_codes }).into_response() 314 224 } 315 225 316 226 #[derive(Deserialize)] 317 227 pub struct DisableTotpInput { 318 - pub password: String, 228 + pub password: PlainPassword, 319 229 pub code: String, 320 230 } 321 231 ··· 333 243 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 334 244 .await 335 245 { 336 - warn!(did = %auth.0.did, "TOTP verification rate limit exceeded"); 337 - return ( 338 - StatusCode::TOO_MANY_REQUESTS, 339 - Json(json!({ 340 - "error": "RateLimitExceeded", 341 - "message": "Too many verification attempts. Please try again in a few minutes." 342 - })), 343 - ) 344 - .into_response(); 246 + warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 247 + return ApiError::RateLimitExceeded(None).into_response(); 345 248 } 346 249 347 - let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", auth.0.did) 250 + let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", &*&auth.0.did) 348 251 .fetch_optional(&state.db) 349 252 .await; 350 253 351 254 let password_hash = match user { 352 255 Ok(Some(row)) => row.password_hash, 353 - Ok(None) => { 354 - return ( 355 - StatusCode::NOT_FOUND, 356 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 357 - ) 358 - .into_response(); 359 - } 256 + Ok(None) => return ApiError::AccountNotFound.into_response(), 360 257 Err(e) => { 361 258 error!("DB error fetching user: {:?}", e); 362 - return ( 363 - StatusCode::INTERNAL_SERVER_ERROR, 364 - Json(json!({"error": "InternalError"})), 365 - ) 366 - .into_response(); 259 + return ApiError::InternalError(None).into_response(); 367 260 } 368 261 }; 369 262 ··· 372 265 .map(|h| bcrypt::verify(&input.password, h).unwrap_or(false)) 373 266 .unwrap_or(false); 374 267 if !password_valid { 375 - return ( 376 - StatusCode::UNAUTHORIZED, 377 - Json(json!({ 378 - "error": "InvalidPassword", 379 - "message": "Password is incorrect" 380 - })), 381 - ) 382 - .into_response(); 268 + return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 383 269 } 384 270 385 271 let totp_row = sqlx::query!( 386 272 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 387 - auth.0.did 273 + &auth.0.did 388 274 ) 389 275 .fetch_optional(&state.db) 390 276 .await; 391 277 392 278 let totp_row = match totp_row { 393 279 Ok(Some(row)) if row.verified => row, 394 - Ok(Some(_)) | Ok(None) => { 395 - return ( 396 - StatusCode::BAD_REQUEST, 397 - Json(json!({ 398 - "error": "TotpNotEnabled", 399 - "message": "TOTP is not enabled for this account" 400 - })), 401 - ) 402 - .into_response(); 403 - } 280 + Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 404 281 Err(e) => { 405 282 error!("DB error fetching TOTP: {:?}", e); 406 - return ( 407 - StatusCode::INTERNAL_SERVER_ERROR, 408 - Json(json!({"error": "InternalError"})), 409 - ) 410 - .into_response(); 283 + return ApiError::InternalError(None).into_response(); 411 284 } 412 285 }; 413 286 ··· 420 293 Ok(s) => s, 421 294 Err(e) => { 422 295 error!("Failed to decrypt TOTP secret: {:?}", e); 423 - return ( 424 - StatusCode::INTERNAL_SERVER_ERROR, 425 - Json(json!({"error": "InternalError"})), 426 - ) 427 - .into_response(); 296 + return ApiError::InternalError(None).into_response(); 428 297 } 429 298 }; 430 299 verify_totp_code(&secret, code) 431 300 }; 432 301 433 302 if !code_valid { 434 - return ( 435 - StatusCode::UNAUTHORIZED, 436 - Json(json!({ 437 - "error": "InvalidCode", 438 - "message": "Invalid verification code" 439 - })), 440 - ) 441 - .into_response(); 303 + return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 442 304 } 443 305 444 306 let mut tx = match state.db.begin().await { 445 307 Ok(tx) => tx, 446 308 Err(e) => { 447 309 error!("Failed to begin transaction: {:?}", e); 448 - return ( 449 - StatusCode::INTERNAL_SERVER_ERROR, 450 - Json(json!({"error": "InternalError"})), 451 - ) 452 - .into_response(); 310 + return ApiError::InternalError(None).into_response(); 453 311 } 454 312 }; 455 313 456 - if let Err(e) = sqlx::query!("DELETE FROM user_totp WHERE did = $1", auth.0.did) 314 + if let Err(e) = sqlx::query!("DELETE FROM user_totp WHERE did = $1", &*&auth.0.did) 457 315 .execute(&mut *tx) 458 316 .await 459 317 { 460 318 error!("Failed to delete TOTP: {:?}", e); 461 - return ( 462 - StatusCode::INTERNAL_SERVER_ERROR, 463 - Json(json!({"error": "InternalError"})), 464 - ) 465 - .into_response(); 319 + return ApiError::InternalError(None).into_response(); 466 320 } 467 321 468 - if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", auth.0.did) 322 + if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", &*&auth.0.did) 469 323 .execute(&mut *tx) 470 324 .await 471 325 { 472 326 error!("Failed to delete backup codes: {:?}", e); 473 - return ( 474 - StatusCode::INTERNAL_SERVER_ERROR, 475 - Json(json!({"error": "InternalError"})), 476 - ) 477 - .into_response(); 327 + return ApiError::InternalError(None).into_response(); 478 328 } 479 329 480 330 if let Err(e) = tx.commit().await { 481 331 error!("Failed to commit transaction: {:?}", e); 482 - return ( 483 - StatusCode::INTERNAL_SERVER_ERROR, 484 - Json(json!({"error": "InternalError"})), 485 - ) 486 - .into_response(); 332 + return ApiError::InternalError(None).into_response(); 487 333 } 488 334 489 - info!(did = %auth.0.did, "TOTP disabled"); 335 + info!(did = %&auth.0.did, "TOTP disabled"); 490 336 491 - (StatusCode::OK, Json(json!({}))).into_response() 337 + EmptyResponse::ok().into_response() 492 338 } 493 339 494 340 #[derive(Serialize)] ··· 500 346 } 501 347 502 348 pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 503 - let totp_row = sqlx::query!("SELECT verified FROM user_totp WHERE did = $1", auth.0.did) 349 + let totp_row = sqlx::query!("SELECT verified FROM user_totp WHERE did = $1", &*&auth.0.did) 504 350 .fetch_optional(&state.db) 505 351 .await; 506 352 ··· 509 355 Ok(None) => false, 510 356 Err(e) => { 511 357 error!("DB error fetching TOTP status: {:?}", e); 512 - return ( 513 - StatusCode::INTERNAL_SERVER_ERROR, 514 - Json(json!({"error": "InternalError"})), 515 - ) 516 - .into_response(); 358 + return ApiError::InternalError(None).into_response(); 517 359 } 518 360 }; 519 361 520 362 let backup_count_row = sqlx::query!( 521 363 "SELECT COUNT(*) as count FROM backup_codes WHERE did = $1 AND used_at IS NULL", 522 - auth.0.did 364 + &auth.0.did 523 365 ) 524 366 .fetch_one(&state.db) 525 367 .await; ··· 536 378 537 379 #[derive(Deserialize)] 538 380 pub struct RegenerateBackupCodesInput { 539 - pub password: String, 381 + pub password: PlainPassword, 540 382 pub code: String, 541 383 } 542 384 ··· 555 397 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 556 398 .await 557 399 { 558 - warn!(did = %auth.0.did, "TOTP verification rate limit exceeded"); 559 - return ( 560 - StatusCode::TOO_MANY_REQUESTS, 561 - Json(json!({ 562 - "error": "RateLimitExceeded", 563 - "message": "Too many verification attempts. Please try again in a few minutes." 564 - })), 565 - ) 566 - .into_response(); 400 + warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 401 + return ApiError::RateLimitExceeded(None).into_response(); 567 402 } 568 403 569 - let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", auth.0.did) 404 + let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", &*&auth.0.did) 570 405 .fetch_optional(&state.db) 571 406 .await; 572 407 573 408 let password_hash = match user { 574 409 Ok(Some(row)) => row.password_hash, 575 - Ok(None) => { 576 - return ( 577 - StatusCode::NOT_FOUND, 578 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 579 - ) 580 - .into_response(); 581 - } 410 + Ok(None) => return ApiError::AccountNotFound.into_response(), 582 411 Err(e) => { 583 412 error!("DB error fetching user: {:?}", e); 584 - return ( 585 - StatusCode::INTERNAL_SERVER_ERROR, 586 - Json(json!({"error": "InternalError"})), 587 - ) 588 - .into_response(); 413 + return ApiError::InternalError(None).into_response(); 589 414 } 590 415 }; 591 416 ··· 594 419 .map(|h| bcrypt::verify(&input.password, h).unwrap_or(false)) 595 420 .unwrap_or(false); 596 421 if !password_valid { 597 - return ( 598 - StatusCode::UNAUTHORIZED, 599 - Json(json!({ 600 - "error": "InvalidPassword", 601 - "message": "Password is incorrect" 602 - })), 603 - ) 604 - .into_response(); 422 + return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 605 423 } 606 424 607 425 let totp_row = sqlx::query!( 608 426 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 609 - auth.0.did 427 + &auth.0.did 610 428 ) 611 429 .fetch_optional(&state.db) 612 430 .await; 613 431 614 432 let totp_row = match totp_row { 615 433 Ok(Some(row)) if row.verified => row, 616 - Ok(Some(_)) | Ok(None) => { 617 - return ( 618 - StatusCode::BAD_REQUEST, 619 - Json(json!({ 620 - "error": "TotpNotEnabled", 621 - "message": "TOTP must be enabled to regenerate backup codes" 622 - })), 623 - ) 624 - .into_response(); 625 - } 434 + Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 626 435 Err(e) => { 627 436 error!("DB error fetching TOTP: {:?}", e); 628 - return ( 629 - StatusCode::INTERNAL_SERVER_ERROR, 630 - Json(json!({"error": "InternalError"})), 631 - ) 632 - .into_response(); 437 + return ApiError::InternalError(None).into_response(); 633 438 } 634 439 }; 635 440 ··· 638 443 Ok(s) => s, 639 444 Err(e) => { 640 445 error!("Failed to decrypt TOTP secret: {:?}", e); 641 - return ( 642 - StatusCode::INTERNAL_SERVER_ERROR, 643 - Json(json!({"error": "InternalError"})), 644 - ) 645 - .into_response(); 446 + return ApiError::InternalError(None).into_response(); 646 447 } 647 448 }; 648 449 649 450 let code = input.code.trim(); 650 451 if !verify_totp_code(&secret, code) { 651 - return ( 652 - StatusCode::UNAUTHORIZED, 653 - Json(json!({ 654 - "error": "InvalidCode", 655 - "message": "Invalid verification code" 656 - })), 657 - ) 658 - .into_response(); 452 + return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 659 453 } 660 454 661 455 let backup_codes = generate_backup_codes(); ··· 663 457 Ok(tx) => tx, 664 458 Err(e) => { 665 459 error!("Failed to begin transaction: {:?}", e); 666 - return ( 667 - StatusCode::INTERNAL_SERVER_ERROR, 668 - Json(json!({"error": "InternalError"})), 669 - ) 670 - .into_response(); 460 + return ApiError::InternalError(None).into_response(); 671 461 } 672 462 }; 673 463 674 - if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", auth.0.did) 464 + if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", &*&auth.0.did) 675 465 .execute(&mut *tx) 676 466 .await 677 467 { 678 468 error!("Failed to clear old backup codes: {:?}", e); 679 - return ( 680 - StatusCode::INTERNAL_SERVER_ERROR, 681 - Json(json!({"error": "InternalError"})), 682 - ) 683 - .into_response(); 469 + return ApiError::InternalError(None).into_response(); 684 470 } 685 471 686 472 for code in &backup_codes { ··· 688 474 Ok(h) => h, 689 475 Err(e) => { 690 476 error!("Failed to hash backup code: {:?}", e); 691 - return ( 692 - StatusCode::INTERNAL_SERVER_ERROR, 693 - Json(json!({"error": "InternalError"})), 694 - ) 695 - .into_response(); 477 + return ApiError::InternalError(None).into_response(); 696 478 } 697 479 }; 698 480 699 481 if let Err(e) = sqlx::query!( 700 482 "INSERT INTO backup_codes (did, code_hash, created_at) VALUES ($1, $2, NOW())", 701 - auth.0.did, 483 + &auth.0.did, 702 484 hash 703 485 ) 704 486 .execute(&mut *tx) 705 487 .await 706 488 { 707 489 error!("Failed to store backup code: {:?}", e); 708 - return ( 709 - StatusCode::INTERNAL_SERVER_ERROR, 710 - Json(json!({"error": "InternalError"})), 711 - ) 712 - .into_response(); 490 + return ApiError::InternalError(None).into_response(); 713 491 } 714 492 } 715 493 716 494 if let Err(e) = tx.commit().await { 717 495 error!("Failed to commit transaction: {:?}", e); 718 - return ( 719 - StatusCode::INTERNAL_SERVER_ERROR, 720 - Json(json!({"error": "InternalError"})), 721 - ) 722 - .into_response(); 496 + return ApiError::InternalError(None).into_response(); 723 497 } 724 498 725 - info!(did = %auth.0.did, "Backup codes regenerated"); 499 + info!(did = %&auth.0.did, "Backup codes regenerated"); 726 500 727 501 Json(RegenerateBackupCodesResponse { backup_codes }).into_response() 728 502 }
+73 -55
src/api/server/trusted_devices.rs
··· 1 + use crate::api::error::ApiError; 2 + use crate::api::SuccessResponse; 1 3 use axum::{ 2 4 Json, 3 5 extract::State, 4 - http::StatusCode, 5 6 response::{IntoResponse, Response}, 6 7 }; 7 8 use chrono::{DateTime, Duration, Utc}; 8 9 use serde::{Deserialize, Serialize}; 9 - use serde_json::json; 10 10 use sqlx::PgPool; 11 11 use tracing::{error, info}; 12 12 ··· 15 15 16 16 const TRUST_DURATION_DAYS: i64 = 30; 17 17 18 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] 19 + #[serde(rename_all = "lowercase")] 20 + pub enum DeviceTrustState { 21 + Untrusted, 22 + Trusted, 23 + Expired, 24 + } 25 + 26 + impl DeviceTrustState { 27 + pub fn from_timestamps( 28 + trusted_at: Option<DateTime<Utc>>, 29 + trusted_until: Option<DateTime<Utc>>, 30 + ) -> Self { 31 + match (trusted_at, trusted_until) { 32 + (Some(_), Some(until)) if until > Utc::now() => Self::Trusted, 33 + (Some(_), Some(_)) => Self::Expired, 34 + _ => Self::Untrusted, 35 + } 36 + } 37 + 38 + pub fn is_trusted(&self) -> bool { 39 + matches!(self, Self::Trusted) 40 + } 41 + 42 + pub fn is_expired(&self) -> bool { 43 + matches!(self, Self::Expired) 44 + } 45 + 46 + pub fn as_str(&self) -> &'static str { 47 + match self { 48 + Self::Untrusted => "untrusted", 49 + Self::Trusted => "trusted", 50 + Self::Expired => "expired", 51 + } 52 + } 53 + } 54 + 18 55 #[derive(Serialize)] 19 56 #[serde(rename_all = "camelCase")] 20 57 pub struct TrustedDevice { ··· 24 61 pub trusted_at: Option<DateTime<Utc>>, 25 62 pub trusted_until: Option<DateTime<Utc>>, 26 63 pub last_seen_at: DateTime<Utc>, 64 + pub trust_state: DeviceTrustState, 27 65 } 28 66 29 67 #[derive(Serialize)] ··· 39 77 JOIN oauth_account_device oad ON od.id = oad.device_id 40 78 WHERE oad.did = $1 AND od.trusted_until IS NOT NULL AND od.trusted_until > NOW() 41 79 ORDER BY od.last_seen_at DESC"#, 42 - auth.0.did 80 + &auth.0.did 43 81 ) 44 82 .fetch_all(&state.db) 45 83 .await; ··· 48 86 Ok(rows) => { 49 87 let devices = rows 50 88 .into_iter() 51 - .map(|row| TrustedDevice { 52 - id: row.id, 53 - user_agent: row.user_agent, 54 - friendly_name: row.friendly_name, 55 - trusted_at: row.trusted_at, 56 - trusted_until: row.trusted_until, 57 - last_seen_at: row.last_seen_at, 89 + .map(|row| { 90 + let trust_state = DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); 91 + TrustedDevice { 92 + id: row.id, 93 + user_agent: row.user_agent, 94 + friendly_name: row.friendly_name, 95 + trusted_at: row.trusted_at, 96 + trusted_until: row.trusted_until, 97 + last_seen_at: row.last_seen_at, 98 + trust_state, 99 + } 58 100 }) 59 101 .collect(); 60 102 Json(ListTrustedDevicesResponse { devices }).into_response() 61 103 } 62 104 Err(e) => { 63 105 error!("DB error: {:?}", e); 64 - ( 65 - StatusCode::INTERNAL_SERVER_ERROR, 66 - Json(json!({"error": "InternalError"})), 67 - ) 68 - .into_response() 106 + ApiError::InternalError(None).into_response() 69 107 } 70 108 } 71 109 } ··· 85 123 r#"SELECT 1 as one FROM oauth_device od 86 124 JOIN oauth_account_device oad ON od.id = oad.device_id 87 125 WHERE oad.did = $1 AND od.id = $2"#, 88 - auth.0.did, 126 + &auth.0.did, 89 127 input.device_id 90 128 ) 91 129 .fetch_optional(&state.db) ··· 94 132 match device_exists { 95 133 Ok(Some(_)) => {} 96 134 Ok(None) => { 97 - return ( 98 - StatusCode::NOT_FOUND, 99 - Json(json!({"error": "DeviceNotFound", "message": "Device not found or not owned by this account"})), 100 - ) 101 - .into_response(); 135 + return ApiError::DeviceNotFound.into_response(); 102 136 } 103 137 Err(e) => { 104 138 error!("DB error: {:?}", e); 105 - return ( 106 - StatusCode::INTERNAL_SERVER_ERROR, 107 - Json(json!({"error": "InternalError"})), 108 - ) 109 - .into_response(); 139 + return ApiError::InternalError(None).into_response(); 110 140 } 111 141 } 112 142 ··· 119 149 120 150 match result { 121 151 Ok(_) => { 122 - info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device revoked"); 123 - Json(json!({"success": true})).into_response() 152 + info!(did = %&auth.0.did, device_id = %input.device_id, "Trusted device revoked"); 153 + SuccessResponse::ok().into_response() 124 154 } 125 155 Err(e) => { 126 156 error!("DB error: {:?}", e); 127 - ( 128 - StatusCode::INTERNAL_SERVER_ERROR, 129 - Json(json!({"error": "InternalError"})), 130 - ) 131 - .into_response() 157 + ApiError::InternalError(None).into_response() 132 158 } 133 159 } 134 160 } ··· 149 175 r#"SELECT 1 as one FROM oauth_device od 150 176 JOIN oauth_account_device oad ON od.id = oad.device_id 151 177 WHERE oad.did = $1 AND od.id = $2"#, 152 - auth.0.did, 178 + &auth.0.did, 153 179 input.device_id 154 180 ) 155 181 .fetch_optional(&state.db) ··· 158 184 match device_exists { 159 185 Ok(Some(_)) => {} 160 186 Ok(None) => { 161 - return ( 162 - StatusCode::NOT_FOUND, 163 - Json(json!({"error": "DeviceNotFound", "message": "Device not found or not owned by this account"})), 164 - ) 165 - .into_response(); 187 + return ApiError::DeviceNotFound.into_response(); 166 188 } 167 189 Err(e) => { 168 190 error!("DB error: {:?}", e); 169 - return ( 170 - StatusCode::INTERNAL_SERVER_ERROR, 171 - Json(json!({"error": "InternalError"})), 172 - ) 173 - .into_response(); 191 + return ApiError::InternalError(None).into_response(); 174 192 } 175 193 } 176 194 ··· 185 203 match result { 186 204 Ok(_) => { 187 205 info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device updated"); 188 - Json(json!({"success": true})).into_response() 206 + SuccessResponse::ok().into_response() 189 207 } 190 208 Err(e) => { 191 209 error!("DB error: {:?}", e); 192 - ( 193 - StatusCode::INTERNAL_SERVER_ERROR, 194 - Json(json!({"error": "InternalError"})), 195 - ) 196 - .into_response() 210 + ApiError::InternalError(None).into_response() 197 211 } 198 212 } 199 213 } 200 214 201 - pub async fn is_device_trusted(db: &PgPool, device_id: &str, did: &str) -> bool { 202 - let result = sqlx::query_scalar!( 203 - r#"SELECT trusted_until FROM oauth_device od 215 + pub async fn get_device_trust_state(db: &PgPool, device_id: &str, did: &str) -> DeviceTrustState { 216 + let result = sqlx::query!( 217 + r#"SELECT trusted_at, trusted_until FROM oauth_device od 204 218 JOIN oauth_account_device oad ON od.id = oad.device_id 205 219 WHERE od.id = $1 AND oad.did = $2"#, 206 220 device_id, ··· 210 224 .await; 211 225 212 226 match result { 213 - Ok(Some(Some(trusted_until))) => trusted_until > Utc::now(), 214 - _ => false, 227 + Ok(Some(row)) => DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until), 228 + _ => DeviceTrustState::Untrusted, 215 229 } 230 + } 231 + 232 + pub async fn is_device_trusted(db: &PgPool, device_id: &str, did: &str) -> bool { 233 + get_device_trust_state(db, device_id, did).await.is_trusted() 216 234 } 217 235 218 236 pub async fn trust_device(db: &PgPool, device_id: &str) -> Result<(), sqlx::Error> {
+8 -10
src/api/server/verify_email.rs
··· 1 - use axum::{Json, extract::State, http::StatusCode}; 1 + use crate::api::error::ApiError; 2 + use crate::types::Did; 3 + use axum::{Json, extract::State}; 2 4 use serde::{Deserialize, Serialize}; 3 - use serde_json::json; 4 5 use tracing::{info, warn}; 5 6 6 7 use crate::state::AppState; ··· 16 17 #[serde(rename_all = "camelCase")] 17 18 pub struct VerifyMigrationEmailOutput { 18 19 pub success: bool, 19 - pub did: String, 20 + pub did: Did, 20 21 } 21 22 22 23 pub async fn verify_migration_email( 23 24 State(state): State<AppState>, 24 25 Json(input): Json<VerifyMigrationEmailInput>, 25 - ) -> Result<Json<VerifyMigrationEmailOutput>, (StatusCode, Json<serde_json::Value>)> { 26 + ) -> Result<Json<VerifyMigrationEmailOutput>, ApiError> { 26 27 let token_input = super::verify_token::VerifyTokenInput { 27 28 token: input.token, 28 29 identifier: input.email, ··· 32 33 33 34 Ok(Json(VerifyMigrationEmailOutput { 34 35 success: result.success, 35 - did: result.did.clone(), 36 + did: result.did.clone().into(), 36 37 })) 37 38 } 38 39 ··· 51 52 pub async fn resend_migration_verification( 52 53 State(state): State<AppState>, 53 54 Json(input): Json<ResendMigrationVerificationInput>, 54 - ) -> Result<Json<ResendMigrationVerificationOutput>, (StatusCode, Json<serde_json::Value>)> { 55 + ) -> Result<Json<ResendMigrationVerificationOutput>, ApiError> { 55 56 let email = input.email.trim().to_lowercase(); 56 57 57 58 let user = sqlx::query!( ··· 62 63 .await 63 64 .map_err(|e| { 64 65 warn!(error = %e, "Database error during resend verification"); 65 - ( 66 - StatusCode::INTERNAL_SERVER_ERROR, 67 - Json(json!({ "error": "InternalError", "message": "Database error" })), 68 - ) 66 + ApiError::InternalError(None) 69 67 })?; 70 68 71 69 let user = match user {
+32 -115
src/api/server/verify_token.rs
··· 1 - use axum::{Json, extract::State, http::StatusCode}; 1 + use crate::api::error::ApiError; 2 + use crate::types::Did; 3 + use axum::{Json, extract::State}; 2 4 use serde::{Deserialize, Serialize}; 3 - use serde_json::json; 4 5 use tracing::{error, info, warn}; 5 6 6 7 use crate::auth::verification_token::{ 7 - VerificationPurpose, VerifyError, normalize_token_input, verify_token_signature, 8 + VerificationPurpose, normalize_token_input, verify_token_signature, 8 9 }; 9 10 use crate::state::AppState; 10 11 ··· 19 20 #[serde(rename_all = "camelCase")] 20 21 pub struct VerifyTokenOutput { 21 22 pub success: bool, 22 - pub did: String, 23 + pub did: Did, 23 24 pub purpose: String, 24 25 pub channel: String, 25 26 } ··· 27 28 pub async fn verify_token( 28 29 State(state): State<AppState>, 29 30 Json(input): Json<VerifyTokenInput>, 30 - ) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> { 31 + ) -> Result<Json<VerifyTokenOutput>, ApiError> { 31 32 verify_token_internal(&state, input).await 32 33 } 33 34 34 35 pub async fn verify_token_internal( 35 36 state: &AppState, 36 37 input: VerifyTokenInput, 37 - ) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> { 38 + ) -> Result<Json<VerifyTokenOutput>, ApiError> { 38 39 let normalized_token = normalize_token_input(&input.token); 39 40 let identifier = input.identifier.trim().to_lowercase(); 40 41 41 - let token_data = match verify_token_signature(&normalized_token) { 42 - Ok(data) => data, 43 - Err(e) => { 44 - let (status, error, message) = match e { 45 - VerifyError::InvalidFormat => ( 46 - StatusCode::BAD_REQUEST, 47 - "InvalidToken", 48 - "The verification token is invalid or malformed", 49 - ), 50 - VerifyError::UnsupportedVersion => ( 51 - StatusCode::BAD_REQUEST, 52 - "InvalidToken", 53 - "This verification token version is not supported", 54 - ), 55 - VerifyError::Expired => ( 56 - StatusCode::BAD_REQUEST, 57 - "ExpiredToken", 58 - "The verification token has expired. Please request a new one.", 59 - ), 60 - VerifyError::InvalidSignature => ( 61 - StatusCode::BAD_REQUEST, 62 - "InvalidToken", 63 - "The verification token signature is invalid", 64 - ), 65 - _ => ( 66 - StatusCode::BAD_REQUEST, 67 - "InvalidToken", 68 - "The verification token is not valid", 69 - ), 70 - }; 71 - warn!(error = ?e, "Token verification failed"); 72 - return Err((status, Json(json!({ "error": error, "message": message })))); 73 - } 74 - }; 42 + let token_data = verify_token_signature(&normalized_token).map_err(|e| { 43 + warn!(error = ?e, "Token verification failed"); 44 + ApiError::from(e) 45 + })?; 75 46 76 47 let expected_hash = crate::auth::verification_token::hash_identifier(&identifier); 77 48 if token_data.identifier_hash != expected_hash { 78 - return Err(( 79 - StatusCode::BAD_REQUEST, 80 - Json( 81 - json!({ "error": "IdentifierMismatch", "message": "The identifier does not match the verification token" }), 82 - ), 83 - )); 49 + return Err(ApiError::IdentifierMismatch); 84 50 } 85 51 86 52 match token_data.purpose { ··· 103 69 did: &str, 104 70 channel: &str, 105 71 identifier: &str, 106 - ) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> { 72 + ) -> Result<Json<VerifyTokenOutput>, ApiError> { 107 73 if channel != "email" { 108 - return Err(( 109 - StatusCode::BAD_REQUEST, 110 - Json( 111 - json!({ "error": "InvalidChannel", "message": "Migration verification is only supported for email" }), 112 - ), 113 - )); 74 + return Err(ApiError::InvalidChannel); 114 75 } 115 76 116 77 let user = sqlx::query!( ··· 121 82 .await 122 83 .map_err(|e| { 123 84 warn!(error = %e, "Database error during migration verification"); 124 - ( 125 - StatusCode::INTERNAL_SERVER_ERROR, 126 - Json(json!({ "error": "InternalError", "message": "Database error" })), 127 - ) 85 + ApiError::InternalError(None) 128 86 })?; 129 87 130 - let user = user.ok_or_else(|| { 131 - ( 132 - StatusCode::NOT_FOUND, 133 - Json(json!({ "error": "AccountNotFound", "message": "No account found for this verification token" })), 134 - ) 135 - })?; 88 + let user = user.ok_or(ApiError::AccountNotFound)?; 136 89 137 90 if user.email.as_ref().map(|e| e.to_lowercase()) != Some(identifier.to_string()) { 138 - return Err(( 139 - StatusCode::BAD_REQUEST, 140 - Json( 141 - json!({ "error": "IdentifierMismatch", "message": "The email address does not match the account" }), 142 - ), 143 - )); 91 + return Err(ApiError::IdentifierMismatch); 144 92 } 145 93 146 94 if !user.email_verified { ··· 152 100 .await 153 101 .map_err(|e| { 154 102 warn!(error = %e, "Failed to update email_verified status"); 155 - ( 156 - StatusCode::INTERNAL_SERVER_ERROR, 157 - Json(json!({ "error": "InternalError", "message": "Failed to verify email" })), 158 - ) 103 + ApiError::InternalError(None) 159 104 })?; 160 105 } 161 106 ··· 163 108 164 109 Ok(Json(VerifyTokenOutput { 165 110 success: true, 166 - did: did.to_string(), 111 + did: did.to_string().into(), 167 112 purpose: "migration".to_string(), 168 113 channel: channel.to_string(), 169 114 })) ··· 174 119 did: &str, 175 120 channel: &str, 176 121 identifier: &str, 177 - ) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> { 122 + ) -> Result<Json<VerifyTokenOutput>, ApiError> { 178 123 let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 179 124 .fetch_one(&state.db) 180 125 .await 181 - .map_err(|_| { 182 - ( 183 - StatusCode::INTERNAL_SERVER_ERROR, 184 - Json(json!({ "error": "InternalError", "message": "User not found" })), 185 - ) 186 - })?; 126 + .map_err(|_| ApiError::InternalError(None))?; 187 127 188 128 let update_result = match channel { 189 129 "email" => sqlx::query!( ··· 207 147 user_id 208 148 ).execute(&state.db).await, 209 149 _ => { 210 - return Err(( 211 - StatusCode::BAD_REQUEST, 212 - Json(json!({ "error": "InvalidChannel", "message": "Invalid channel" })), 213 - )); 150 + return Err(ApiError::InvalidChannel); 214 151 } 215 152 }; 216 153 ··· 221 158 .map(|db| db.is_unique_violation()) 222 159 .unwrap_or(false) 223 160 { 224 - return Err(( 225 - StatusCode::BAD_REQUEST, 226 - Json(json!({ "error": "EmailTaken", "message": "Email already in use" })), 227 - )); 161 + return Err(ApiError::EmailTaken); 228 162 } 229 - return Err(( 230 - StatusCode::INTERNAL_SERVER_ERROR, 231 - Json(json!({ "error": "InternalError", "message": "Failed to update channel" })), 232 - )); 163 + return Err(ApiError::InternalError(None)); 233 164 } 234 165 235 166 info!(did = %did, channel = %channel, "Channel verified successfully"); 236 167 237 168 Ok(Json(VerifyTokenOutput { 238 169 success: true, 239 - did: did.to_string(), 170 + did: did.to_string().into(), 240 171 purpose: "channel_update".to_string(), 241 172 channel: channel.to_string(), 242 173 })) ··· 247 178 did: &str, 248 179 channel: &str, 249 180 _identifier: &str, 250 - ) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> { 181 + ) -> Result<Json<VerifyTokenOutput>, ApiError> { 251 182 let user = sqlx::query!( 252 183 "SELECT id, handle, email, email_verified, discord_verified, telegram_verified, signal_verified FROM users WHERE did = $1", 253 184 did ··· 256 187 .await 257 188 .map_err(|e| { 258 189 warn!(error = %e, "Database error during signup verification"); 259 - ( 260 - StatusCode::INTERNAL_SERVER_ERROR, 261 - Json(json!({ "error": "InternalError", "message": "Database error" })), 262 - ) 190 + ApiError::InternalError(None) 263 191 })?; 264 192 265 - let user = user.ok_or_else(|| { 266 - ( 267 - StatusCode::NOT_FOUND, 268 - Json(json!({ "error": "AccountNotFound", "message": "No account found for this verification token" })), 269 - ) 270 - })?; 193 + let user = user.ok_or(ApiError::AccountNotFound)?; 271 194 272 195 let is_verified = user.email_verified 273 196 || user.discord_verified ··· 277 200 info!(did = %did, "Account already verified"); 278 201 return Ok(Json(VerifyTokenOutput { 279 202 success: true, 280 - did: did.to_string(), 203 + did: did.to_string().into(), 281 204 purpose: "signup".to_string(), 282 205 channel: channel.to_string(), 283 206 })); ··· 317 240 .await 318 241 } 319 242 _ => { 320 - return Err(( 321 - StatusCode::BAD_REQUEST, 322 - Json(json!({ "error": "InvalidChannel", "message": "Invalid channel" })), 323 - )); 243 + return Err(ApiError::InvalidChannel); 324 244 } 325 245 }; 326 246 327 247 update_result.map_err(|e| { 328 248 warn!(error = %e, "Failed to update channel verified status"); 329 - ( 330 - StatusCode::INTERNAL_SERVER_ERROR, 331 - Json(json!({ "error": "InternalError", "message": "Failed to verify channel" })), 332 - ) 249 + ApiError::InternalError(None) 333 250 })?; 334 251 335 252 info!(did = %did, channel = %channel, "Signup verified successfully"); 336 253 337 254 Ok(Json(VerifyTokenOutput { 338 255 success: true, 339 - did: did.to_string(), 256 + did: did.to_string().into(), 340 257 purpose: "signup".to_string(), 341 258 channel: channel.to_string(), 342 259 }))
+7 -25
src/api/temp.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::auth::{extract_bearer_token_from_header, validate_bearer_token}; 2 3 use crate::state::AppState; 3 4 use axum::{ 4 5 Json, 5 6 extract::State, 6 - http::{HeaderMap, StatusCode}, 7 + http::HeaderMap, 7 8 response::{IntoResponse, Response}, 8 9 }; 9 10 use cid::Cid; 10 11 use jacquard_repo::storage::BlockStore; 11 12 use serde::{Deserialize, Serialize}; 12 - use serde_json::json; 13 13 use std::str::FromStr; 14 14 15 15 #[derive(Serialize)] ··· 28 28 && let Ok(user) = validate_bearer_token(&state.db, &token).await 29 29 && user.is_oauth 30 30 { 31 - return ( 32 - StatusCode::FORBIDDEN, 33 - Json(json!({ 34 - "error": "Forbidden", 35 - "message": "OAuth credentials are not supported for this endpoint" 36 - })), 37 - ) 38 - .into_response(); 31 + return ApiError::Forbidden.into_response(); 39 32 } 40 33 Json(CheckSignupQueueOutput { 41 34 activated: true, ··· 62 55 headers: HeaderMap, 63 56 Json(input): Json<DereferenceScopeInput>, 64 57 ) -> Response { 65 - let token = match extract_bearer_token_from_header( 58 + let Some(token) = extract_bearer_token_from_header( 66 59 headers.get("Authorization").and_then(|h| h.to_str().ok()), 67 - ) { 68 - Some(t) => t, 69 - None => { 70 - return ( 71 - StatusCode::UNAUTHORIZED, 72 - Json(json!({"error": "AuthenticationRequired"})), 73 - ) 74 - .into_response(); 75 - } 60 + ) else { 61 + return ApiError::AuthenticationRequired.into_response(); 76 62 }; 77 63 78 64 if validate_bearer_token(&state.db, &token).await.is_err() { 79 - return ( 80 - StatusCode::UNAUTHORIZED, 81 - Json(json!({"error": "AuthenticationFailed"})), 82 - ) 83 - .into_response(); 65 + return ApiError::AuthenticationFailed(None).into_response(); 84 66 } 85 67 86 68 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect();
+195
src/api/validation.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use std::fmt; 3 + use std::ops::Deref; 4 + 1 5 pub const MAX_EMAIL_LENGTH: usize = 254; 2 6 pub const MAX_LOCAL_PART_LENGTH: usize = 64; 3 7 pub const MAX_DOMAIN_LENGTH: usize = 253; ··· 8 12 pub const MAX_HANDLE_LENGTH: usize = 253; 9 13 pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18; 10 14 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(), true)?; 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 + #[derive(Debug, Clone, PartialEq, Eq)] 66 + pub enum EmailValidationError { 67 + Empty, 68 + TooLong, 69 + MissingAtSign, 70 + EmptyLocalPart, 71 + LocalPartTooLong, 72 + InvalidLocalPart, 73 + EmptyDomain, 74 + DomainTooLong, 75 + MissingDomainDot, 76 + InvalidDomainLabel, 77 + } 78 + 79 + impl fmt::Display for EmailValidationError { 80 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 + match self { 82 + Self::Empty => write!(f, "Email cannot be empty"), 83 + Self::TooLong => write!(f, "Email exceeds maximum length of {} characters", MAX_EMAIL_LENGTH), 84 + Self::MissingAtSign => write!(f, "Email must contain @"), 85 + Self::EmptyLocalPart => write!(f, "Email local part cannot be empty"), 86 + Self::LocalPartTooLong => write!(f, "Email local part exceeds maximum length"), 87 + Self::InvalidLocalPart => write!(f, "Email local part contains invalid characters"), 88 + Self::EmptyDomain => write!(f, "Email domain cannot be empty"), 89 + Self::DomainTooLong => write!(f, "Email domain exceeds maximum length"), 90 + Self::MissingDomainDot => write!(f, "Email domain must contain a dot"), 91 + Self::InvalidDomainLabel => write!(f, "Email domain contains invalid label"), 92 + } 93 + } 94 + } 95 + 96 + impl std::error::Error for EmailValidationError {} 97 + 98 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 99 + #[serde(try_from = "String", into = "String")] 100 + pub struct ValidatedEmail(String); 101 + 102 + impl ValidatedEmail { 103 + pub fn new(email: impl AsRef<str>) -> Result<Self, EmailValidationError> { 104 + let email = email.as_ref().trim(); 105 + validate_email_detailed(email)?; 106 + Ok(Self(email.to_string())) 107 + } 108 + 109 + pub fn as_str(&self) -> &str { 110 + &self.0 111 + } 112 + 113 + pub fn into_inner(self) -> String { 114 + self.0 115 + } 116 + 117 + pub fn local_part(&self) -> &str { 118 + self.0.rsplitn(2, '@').nth(1).unwrap_or("") 119 + } 120 + 121 + pub fn domain(&self) -> &str { 122 + self.0.rsplitn(2, '@').next().unwrap_or("") 123 + } 124 + } 125 + 126 + impl Deref for ValidatedEmail { 127 + type Target = str; 128 + fn deref(&self) -> &Self::Target { 129 + &self.0 130 + } 131 + } 132 + 133 + impl fmt::Display for ValidatedEmail { 134 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 135 + write!(f, "{}", self.0) 136 + } 137 + } 138 + 139 + impl TryFrom<String> for ValidatedEmail { 140 + type Error = EmailValidationError; 141 + fn try_from(value: String) -> Result<Self, Self::Error> { 142 + Self::new(value) 143 + } 144 + } 145 + 146 + impl From<ValidatedEmail> for String { 147 + fn from(email: ValidatedEmail) -> Self { 148 + email.0 149 + } 150 + } 151 + 152 + fn validate_email_detailed(email: &str) -> Result<(), EmailValidationError> { 153 + if email.is_empty() { 154 + return Err(EmailValidationError::Empty); 155 + } 156 + if email.len() > MAX_EMAIL_LENGTH { 157 + return Err(EmailValidationError::TooLong); 158 + } 159 + let parts: Vec<&str> = email.rsplitn(2, '@').collect(); 160 + if parts.len() != 2 { 161 + return Err(EmailValidationError::MissingAtSign); 162 + } 163 + let domain = parts[0]; 164 + let local = parts[1]; 165 + if local.is_empty() { 166 + return Err(EmailValidationError::EmptyLocalPart); 167 + } 168 + if local.len() > MAX_LOCAL_PART_LENGTH { 169 + return Err(EmailValidationError::LocalPartTooLong); 170 + } 171 + if local.starts_with('.') || local.ends_with('.') || local.contains("..") { 172 + return Err(EmailValidationError::InvalidLocalPart); 173 + } 174 + for c in local.chars() { 175 + if !c.is_ascii_alphanumeric() && !EMAIL_LOCAL_SPECIAL_CHARS.contains(c) { 176 + return Err(EmailValidationError::InvalidLocalPart); 177 + } 178 + } 179 + if domain.is_empty() { 180 + return Err(EmailValidationError::EmptyDomain); 181 + } 182 + if domain.len() > MAX_DOMAIN_LENGTH { 183 + return Err(EmailValidationError::DomainTooLong); 184 + } 185 + if !domain.contains('.') { 186 + return Err(EmailValidationError::MissingDomainDot); 187 + } 188 + for label in domain.split('.') { 189 + if label.is_empty() || label.len() > MAX_DOMAIN_LABEL_LENGTH { 190 + return Err(EmailValidationError::InvalidDomainLabel); 191 + } 192 + if label.starts_with('-') || label.ends_with('-') { 193 + return Err(EmailValidationError::InvalidDomainLabel); 194 + } 195 + for c in label.chars() { 196 + if !c.is_ascii_alphanumeric() && c != '-' { 197 + return Err(EmailValidationError::InvalidDomainLabel); 198 + } 199 + } 200 + } 201 + Ok(()) 202 + } 203 + 11 204 #[derive(Debug, PartialEq)] 12 205 pub enum HandleValidationError { 13 206 Empty, ··· 49 242 } 50 243 } 51 244 } 245 + 246 + impl std::error::Error for HandleValidationError {} 52 247 53 248 pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> { 54 249 validate_service_handle(handle, false)
+3 -3
src/api/verification.rs
··· 1 + use crate::api::SuccessResponse; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, ··· 5 6 response::{IntoResponse, Response}, 6 7 }; 7 8 use serde::Deserialize; 8 - use serde_json::json; 9 9 10 10 #[derive(Deserialize)] 11 11 #[serde(rename_all = "camelCase")] ··· 25 25 }; 26 26 27 27 match crate::api::server::verify_token_internal(&state, token_input).await { 28 - Ok(output) => Json(json!({"success": output.success})).into_response(), 29 - Err((status, err_json)) => (status, err_json).into_response(), 28 + Ok(_output) => SuccessResponse::ok().into_response(), 29 + Err(e) => e.into_response(), 30 30 } 31 31 }
+5 -16
src/appview/mod.rs
··· 41 41 pub did: String, 42 42 } 43 43 44 + #[derive(Clone)] 44 45 pub struct DidResolver { 45 - did_cache: RwLock<HashMap<String, CachedDid>>, 46 - did_doc_cache: RwLock<HashMap<String, CachedDidDocument>>, 46 + did_cache: Arc<RwLock<HashMap<String, CachedDid>>>, 47 + did_doc_cache: Arc<RwLock<HashMap<String, CachedDidDocument>>>, 47 48 client: Client, 48 49 cache_ttl: Duration, 49 50 plc_directory_url: String, 50 - } 51 - 52 - impl Clone for DidResolver { 53 - fn clone(&self) -> Self { 54 - Self { 55 - did_cache: RwLock::new(HashMap::new()), 56 - did_doc_cache: RwLock::new(HashMap::new()), 57 - client: self.client.clone(), 58 - cache_ttl: self.cache_ttl, 59 - plc_directory_url: self.plc_directory_url.clone(), 60 - } 61 - } 62 51 } 63 52 64 53 impl DidResolver { ··· 81 70 info!("DID resolver initialized"); 82 71 83 72 Self { 84 - did_cache: RwLock::new(HashMap::new()), 85 - did_doc_cache: RwLock::new(HashMap::new()), 73 + did_cache: Arc::new(RwLock::new(HashMap::new())), 74 + did_doc_cache: Arc::new(RwLock::new(HashMap::new())), 86 75 client, 87 76 cache_ttl: Duration::from_secs(cache_ttl_secs), 88 77 plc_directory_url,
+6 -45
src/auth/extractor.rs
··· 1 1 use axum::{ 2 - Json, 3 2 extract::FromRequestParts, 4 - http::{StatusCode, header::AUTHORIZATION, request::Parts}, 3 + http::{header::AUTHORIZATION, request::Parts}, 5 4 response::{IntoResponse, Response}, 6 5 }; 7 - use serde_json::json; 8 6 9 7 use super::{ 10 8 AuthenticatedUser, TokenValidationError, validate_bearer_token_cached, 11 9 validate_bearer_token_cached_allow_deactivated, validate_token_with_dpop, 12 10 }; 11 + use crate::api::error::ApiError; 13 12 use crate::state::AppState; 14 13 use crate::util::build_full_url; 15 14 ··· 28 27 29 28 impl IntoResponse for AuthError { 30 29 fn into_response(self) -> Response { 31 - let (status, error, message) = match self { 32 - AuthError::MissingToken => ( 33 - StatusCode::UNAUTHORIZED, 34 - "AuthenticationRequired", 35 - "Authorization header is required", 36 - ), 37 - AuthError::InvalidFormat => ( 38 - StatusCode::UNAUTHORIZED, 39 - "InvalidToken", 40 - "Invalid authorization header format", 41 - ), 42 - AuthError::AuthenticationFailed => ( 43 - StatusCode::UNAUTHORIZED, 44 - "InvalidToken", 45 - "Token could not be verified", 46 - ), 47 - AuthError::TokenExpired => ( 48 - StatusCode::UNAUTHORIZED, 49 - "ExpiredToken", 50 - "Token has expired", 51 - ), 52 - AuthError::AccountDeactivated => ( 53 - StatusCode::UNAUTHORIZED, 54 - "AccountDeactivated", 55 - "Account is deactivated", 56 - ), 57 - AuthError::AccountTakedown => ( 58 - StatusCode::UNAUTHORIZED, 59 - "AccountTakedown", 60 - "Account has been taken down", 61 - ), 62 - AuthError::AdminRequired => ( 63 - StatusCode::FORBIDDEN, 64 - "AdminRequired", 65 - "This action requires admin privileges", 66 - ), 67 - }; 68 - 69 - (status, Json(json!({ "error": error, "message": message }))).into_response() 30 + ApiError::from(self).into_response() 70 31 } 71 32 } 72 33 ··· 185 146 Err(_) => Err(AuthError::AuthenticationFailed), 186 147 } 187 148 } else { 188 - match validate_bearer_token_cached(&state.db, &state.cache, &extracted.token).await { 149 + match validate_bearer_token_cached(&state.db, state.cache.as_ref(), &extracted.token).await { 189 150 Ok(user) => Ok(BearerAuth(user)), 190 151 Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated), 191 152 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), ··· 239 200 } else { 240 201 match validate_bearer_token_cached_allow_deactivated( 241 202 &state.db, 242 - &state.cache, 203 + state.cache.as_ref(), 243 204 &extracted.token, 244 205 ) 245 206 .await ··· 301 262 Err(_) => return Err(AuthError::AuthenticationFailed), 302 263 } 303 264 } else { 304 - match validate_bearer_token_cached(&state.db, &state.cache, &extracted.token).await { 265 + match validate_bearer_token_cached(&state.db, state.cache.as_ref(), &extracted.token).await { 305 266 Ok(user) => user, 306 267 Err(TokenValidationError::AccountDeactivated) => { 307 268 return Err(AuthError::AccountDeactivated);
+42 -22
src/auth/mod.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 use sqlx::PgPool; 3 3 use std::fmt; 4 - use std::sync::Arc; 5 4 use std::time::Duration; 6 5 6 + use crate::types::Did; 7 + use crate::AccountStatus; 7 8 use crate::cache::Cache; 8 9 use crate::oauth::scopes::ScopePermissions; 9 10 ··· 66 67 } 67 68 68 69 pub struct AuthenticatedUser { 69 - pub did: String, 70 + pub did: Did, 70 71 pub key_bytes: Option<Vec<u8>>, 71 72 pub is_oauth: bool, 72 73 pub is_admin: bool, 73 - pub is_takendown: bool, 74 + pub status: AccountStatus, 74 75 pub scope: Option<String>, 75 - pub controller_did: Option<String>, 76 + pub controller_did: Option<Did>, 76 77 } 77 78 78 79 impl AuthenticatedUser { ··· 87 88 } 88 89 ScopePermissions::from_scope_string(self.scope.as_deref()) 89 90 } 91 + 92 + pub fn is_takendown(&self) -> bool { 93 + self.status.is_takendown() 94 + } 90 95 } 91 96 92 97 pub async fn validate_bearer_token( ··· 105 110 106 111 pub async fn validate_bearer_token_cached( 107 112 db: &PgPool, 108 - cache: &Arc<dyn Cache>, 113 + cache: &dyn Cache, 109 114 token: &str, 110 115 ) -> Result<AuthenticatedUser, TokenValidationError> { 111 116 validate_bearer_token_with_options_internal(db, Some(cache), token, false, false).await ··· 113 118 114 119 pub async fn validate_bearer_token_cached_allow_deactivated( 115 120 db: &PgPool, 116 - cache: &Arc<dyn Cache>, 121 + cache: &dyn Cache, 117 122 token: &str, 118 123 ) -> Result<AuthenticatedUser, TokenValidationError> { 119 124 validate_bearer_token_with_options_internal(db, Some(cache), token, true, false).await ··· 135 140 136 141 async fn validate_bearer_token_with_options_internal( 137 142 db: &PgPool, 138 - cache: Option<&Arc<dyn Cache>>, 143 + cache: Option<&dyn Cache>, 139 144 token: &str, 140 145 allow_deactivated: bool, 141 146 allow_takendown: bool, ··· 324 329 } 325 330 326 331 if session_valid { 327 - let controller_did = token_data.claims.act.as_ref().map(|a| a.sub.clone()); 332 + let controller_did = token_data 333 + .claims 334 + .act 335 + .as_ref() 336 + .map(|a| Did::new_unchecked(a.sub.clone())); 337 + let status = AccountStatus::from_db_fields( 338 + takedown_ref.as_deref(), 339 + deactivated_at, 340 + ); 328 341 return Ok(AuthenticatedUser { 329 - did: did.clone(), 342 + did: Did::new_unchecked(did.clone()), 330 343 key_bytes: Some(decrypted_key), 331 344 is_oauth: false, 332 345 is_admin, 333 - is_takendown: takedown_ref.is_some(), 346 + status, 334 347 scope: token_data.claims.scope.clone(), 335 348 controller_did, 336 349 }); ··· 359 372 .ok() 360 373 .flatten() 361 374 { 362 - if !allow_deactivated && oauth_token.deactivated_at.is_some() { 375 + let status = AccountStatus::from_db_fields( 376 + oauth_token.takedown_ref.as_deref(), 377 + oauth_token.deactivated_at, 378 + ); 379 + 380 + if !allow_deactivated && status.is_deactivated() { 363 381 return Err(TokenValidationError::AccountDeactivated); 364 382 } 365 383 366 - let is_takendown = oauth_token.takedown_ref.is_some(); 367 - if !allow_takendown && is_takendown { 384 + if !allow_takendown && status.is_takendown() { 368 385 return Err(TokenValidationError::AccountTakedown); 369 386 } 370 387 ··· 378 395 None 379 396 }; 380 397 return Ok(AuthenticatedUser { 381 - did: oauth_token.did, 398 + did: Did::new_unchecked(oauth_token.did), 382 399 key_bytes, 383 400 is_oauth: true, 384 401 is_admin: oauth_token.is_admin, 385 - is_takendown, 402 + status, 386 403 scope: oauth_info.scope, 387 - controller_did: oauth_info.controller_did, 404 + controller_did: oauth_info.controller_did.map(Did::new_unchecked), 388 405 }); 389 406 } else { 390 407 return Err(TokenValidationError::TokenExpired); ··· 394 411 Err(TokenValidationError::AuthenticationFailed) 395 412 } 396 413 397 - pub async fn invalidate_auth_cache(cache: &Arc<dyn Cache>, did: &str) { 414 + pub async fn invalidate_auth_cache(cache: &dyn Cache, did: &str) { 398 415 let key_cache_key = format!("auth:key:{}", did); 399 416 let status_cache_key = format!("auth:status:{}", did); 400 417 let _ = cache.delete(&key_cache_key).await; ··· 442 459 let Some(user_info) = user_info else { 443 460 return Err(TokenValidationError::AuthenticationFailed); 444 461 }; 445 - if !allow_deactivated && user_info.deactivated_at.is_some() { 462 + let status = AccountStatus::from_db_fields( 463 + user_info.takedown_ref.as_deref(), 464 + user_info.deactivated_at, 465 + ); 466 + if !allow_deactivated && status.is_deactivated() { 446 467 return Err(TokenValidationError::AccountDeactivated); 447 468 } 448 - let is_takendown = user_info.takedown_ref.is_some(); 449 - if is_takendown { 469 + if status.is_takendown() { 450 470 return Err(TokenValidationError::AccountTakedown); 451 471 } 452 472 let key_bytes = if let (Some(kb), Some(ev)) = ··· 457 477 None 458 478 }; 459 479 Ok(AuthenticatedUser { 460 - did: result.did, 480 + did: Did::new_unchecked(result.did), 461 481 key_bytes, 462 482 is_oauth: true, 463 483 is_admin: user_info.is_admin, 464 - is_takendown, 484 + status, 465 485 scope: result.scope, 466 486 controller_did: None, 467 487 })
+16 -52
src/auth/scope_check.rs
··· 1 1 #![allow(clippy::result_large_err)] 2 2 3 - use axum::http::StatusCode; 4 3 use axum::response::{IntoResponse, Response}; 5 - use serde_json::json; 6 4 5 + use crate::api::error::ApiError; 7 6 use crate::oauth::scopes::{ 8 7 AccountAction, AccountAttr, IdentityAttr, RepoAction, ScopePermissions, 9 8 }; ··· 28 27 } 29 28 30 29 let permissions = ScopePermissions::from_scope_string(scope); 31 - permissions.assert_repo(action, collection).map_err(|e| { 32 - ( 33 - StatusCode::FORBIDDEN, 34 - axum::Json(json!({ 35 - "error": "InsufficientScope", 36 - "message": e.to_string() 37 - })), 38 - ) 39 - .into_response() 40 - }) 30 + permissions 31 + .assert_repo(action, collection) 32 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 41 33 } 42 34 43 35 pub fn check_blob_scope(is_oauth: bool, scope: Option<&str>, mime: &str) -> Result<(), Response> { ··· 46 38 } 47 39 48 40 let permissions = ScopePermissions::from_scope_string(scope); 49 - permissions.assert_blob(mime).map_err(|e| { 50 - ( 51 - StatusCode::FORBIDDEN, 52 - axum::Json(json!({ 53 - "error": "InsufficientScope", 54 - "message": e.to_string() 55 - })), 56 - ) 57 - .into_response() 58 - }) 41 + permissions 42 + .assert_blob(mime) 43 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 59 44 } 60 45 61 46 pub fn check_rpc_scope( ··· 69 54 } 70 55 71 56 let permissions = ScopePermissions::from_scope_string(scope); 72 - permissions.assert_rpc(aud, lxm).map_err(|e| { 73 - ( 74 - StatusCode::FORBIDDEN, 75 - axum::Json(json!({ 76 - "error": "InsufficientScope", 77 - "message": e.to_string() 78 - })), 79 - ) 80 - .into_response() 81 - }) 57 + permissions 58 + .assert_rpc(aud, lxm) 59 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 82 60 } 83 61 84 62 pub fn check_account_scope( ··· 92 70 } 93 71 94 72 let permissions = ScopePermissions::from_scope_string(scope); 95 - permissions.assert_account(attr, action).map_err(|e| { 96 - ( 97 - StatusCode::FORBIDDEN, 98 - axum::Json(json!({ 99 - "error": "InsufficientScope", 100 - "message": e.to_string() 101 - })), 102 - ) 103 - .into_response() 104 - }) 73 + permissions 74 + .assert_account(attr, action) 75 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 105 76 } 106 77 107 78 pub fn check_identity_scope( ··· 114 85 } 115 86 116 87 let permissions = ScopePermissions::from_scope_string(scope); 117 - permissions.assert_identity(attr).map_err(|e| { 118 - ( 119 - StatusCode::FORBIDDEN, 120 - axum::Json(json!({ 121 - "error": "InsufficientScope", 122 - "message": e.to_string() 123 - })), 124 - ) 125 - .into_response() 126 - }) 88 + permissions 89 + .assert_identity(attr) 90 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 127 91 }
+10 -10
src/comms/service.rs
··· 257 257 pub struct UserCommsPrefs { 258 258 pub channel: CommsChannel, 259 259 pub email: Option<String>, 260 - pub handle: String, 260 + pub handle: crate::types::Handle, 261 261 pub locale: String, 262 262 } 263 263 ··· 282 282 Ok(UserCommsPrefs { 283 283 channel: row.channel, 284 284 email: row.email, 285 - handle: row.handle, 285 + handle: row.handle.into(), 286 286 locale: row.preferred_locale.unwrap_or_else(|| "en".to_string()), 287 287 }) 288 288 } ··· 305 305 user_id, 306 306 prefs.channel, 307 307 super::types::CommsType::Welcome, 308 - prefs.email.clone().unwrap_or_default(), 308 + prefs.email.unwrap_or_default(), 309 309 Some(subject), 310 310 body, 311 311 ), ··· 332 332 user_id, 333 333 prefs.channel, 334 334 super::types::CommsType::PasswordReset, 335 - prefs.email.clone().unwrap_or_default(), 335 + prefs.email.unwrap_or_default(), 336 336 Some(subject), 337 337 body, 338 338 ), ··· 388 388 ) -> Result<Uuid, sqlx::Error> { 389 389 let prefs = get_user_comms_prefs(db, user_id).await?; 390 390 let strings = get_strings(&prefs.locale); 391 - let current_email = prefs.email.clone().unwrap_or_default(); 391 + let current_email = prefs.email.unwrap_or_default(); 392 392 let verify_page = format!("https://{}/app/verify?type=email-update", hostname); 393 393 let verify_link = format!( 394 394 "https://{}/app/verify?type=email-update&token={}", ··· 437 437 user_id, 438 438 prefs.channel, 439 439 super::types::CommsType::AccountDeletion, 440 - prefs.email.clone().unwrap_or_default(), 440 + prefs.email.unwrap_or_default(), 441 441 Some(subject), 442 442 body, 443 443 ), ··· 464 464 user_id, 465 465 prefs.channel, 466 466 super::types::CommsType::PlcOperation, 467 - prefs.email.clone().unwrap_or_default(), 467 + prefs.email.unwrap_or_default(), 468 468 Some(subject), 469 469 body, 470 470 ), ··· 491 491 user_id, 492 492 prefs.channel, 493 493 super::types::CommsType::TwoFactorCode, 494 - prefs.email.clone().unwrap_or_default(), 494 + prefs.email.unwrap_or_default(), 495 495 Some(subject), 496 496 body, 497 497 ), ··· 518 518 user_id, 519 519 prefs.channel, 520 520 super::types::CommsType::PasskeyRecovery, 521 - prefs.email.clone().unwrap_or_default(), 521 + prefs.email.unwrap_or_default(), 522 522 Some(subject), 523 523 body, 524 524 ), ··· 665 665 user_id, 666 666 channel, 667 667 super::types::CommsType::LegacyLoginAlert, 668 - prefs.email.clone().unwrap_or_default(), 668 + prefs.email.unwrap_or_default(), 669 669 Some(subject), 670 670 body, 671 671 ),
+3 -2
src/delegation/db.rs
··· 1 + use crate::types::Handle; 1 2 use chrono::{DateTime, Utc}; 2 3 use serde::{Deserialize, Serialize}; 3 4 use sqlx::PgPool; ··· 18 19 #[derive(Debug, Clone, Serialize, Deserialize)] 19 20 pub struct DelegatedAccountInfo { 20 21 pub did: String, 21 - pub handle: String, 22 + pub handle: Handle, 22 23 pub granted_scopes: String, 23 24 pub granted_at: DateTime<Utc>, 24 25 } ··· 26 27 #[derive(Debug, Clone, Serialize, Deserialize)] 27 28 pub struct ControllerInfo { 28 29 pub did: String, 29 - pub handle: String, 30 + pub handle: Handle, 30 31 pub granted_scopes: String, 31 32 pub granted_at: DateTime<Utc>, 32 33 pub is_active: bool,
+6 -2
src/lib.rs
··· 19 19 pub mod state; 20 20 pub mod storage; 21 21 pub mod sync; 22 + pub mod types; 22 23 pub mod util; 23 24 pub mod validation; 24 25 25 26 use api::proxy::XrpcProxyLayer; 27 + pub use sync::util::AccountStatus; 28 + pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey}; 26 29 use axum::{ 27 30 Json, Router, 28 31 extract::DefaultBodyLimit, ··· 33 36 use http::StatusCode; 34 37 use serde_json::json; 35 38 use state::AppState; 36 - use tower::{Layer, ServiceBuilder}; 39 + use tower::ServiceBuilder; 37 40 use tower_http::cors::{Any, CorsLayer}; 38 41 use tower_http::services::{ServeDir, ServeFile}; 39 42 ··· 571 574 let router = Router::new() 572 575 .nest_service("/xrpc", xrpc_service) 573 576 .nest("/oauth", oauth_router) 577 + .nest("/.well-known", well_known_router) 574 578 .route("/metrics", get(metrics::metrics_handler)) 575 579 .route("/health", get(api::server::health)) 576 580 .route("/robots.txt", get(api::server::robots_txt)) ··· 606 610 607 611 let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(&index_path)); 608 612 609 - router 613 + return router 610 614 .route_service("/", ServeFile::new(&homepage_file)) 611 615 .nest("/app", spa_router) 612 616 .fallback_service(serve_dir);
+6 -4
src/main.rs
··· 35 35 let backfill_db = state.db.clone(); 36 36 let backfill_block_store = state.block_store.clone(); 37 37 tokio::spawn(async move { 38 - backfill_genesis_commit_blocks(&backfill_db, backfill_block_store.clone()).await; 39 - backfill_repo_rev(&backfill_db, backfill_block_store.clone()).await; 40 - backfill_user_blocks(&backfill_db, backfill_block_store.clone()).await; 41 - backfill_record_blobs(&backfill_db, backfill_block_store).await; 38 + tokio::join!( 39 + backfill_genesis_commit_blocks(&backfill_db, backfill_block_store.clone()), 40 + backfill_repo_rev(&backfill_db, backfill_block_store.clone()), 41 + backfill_user_blocks(&backfill_db, backfill_block_store.clone()), 42 + backfill_record_blobs(&backfill_db, backfill_block_store), 43 + ); 42 44 }); 43 45 44 46 let mut comms_service = CommsService::new(state.db.clone());
+3 -2
src/oauth/db/device.rs
··· 1 1 use super::super::{DeviceData, OAuthError}; 2 + use crate::types::Handle; 2 3 use chrono::{DateTime, Utc}; 3 4 use sqlx::PgPool; 4 5 5 6 pub struct DeviceAccountRow { 6 7 pub did: String, 7 - pub handle: String, 8 + pub handle: Handle, 8 9 pub email: Option<String>, 9 10 pub last_used_at: DateTime<Utc>, 10 11 } ··· 116 117 .into_iter() 117 118 .map(|r| DeviceAccountRow { 118 119 did: r.did, 119 - handle: r.handle, 120 + handle: r.handle.into(), 120 121 email: r.email, 121 122 last_used_at: r.last_used_at, 122 123 })
+7 -6
src/oauth/db/mod.rs
··· 16 16 pub use request::{ 17 17 consume_authorization_request_by_code, create_authorization_request, 18 18 delete_authorization_request, delete_expired_authorization_requests, get_authorization_request, 19 - mark_request_authenticated, set_authorization_did, set_controller_did, set_request_did, 20 - update_authorization_request, update_request_scope, 19 + get_authorization_request_with_state, mark_request_authenticated, set_authorization_did, 20 + set_controller_did, set_request_did, update_authorization_request, update_request_scope, 21 21 }; 22 22 pub use scope_preference::{ 23 23 ScopePreference, delete_scope_preferences, get_scope_preferences, should_show_consent, 24 24 upsert_scope_preferences, 25 25 }; 26 26 pub use token::{ 27 - check_refresh_token_used, count_tokens_for_user, create_token, delete_oldest_tokens_for_user, 28 - delete_token, delete_token_family, enforce_token_limit_for_user, get_token_by_id, 29 - get_token_by_previous_refresh_token, get_token_by_refresh_token, list_tokens_for_user, 30 - revoke_tokens_for_client, revoke_tokens_for_controller, rotate_token, 27 + RefreshTokenLookup, check_refresh_token_used, count_tokens_for_user, create_token, 28 + delete_oldest_tokens_for_user, delete_token, delete_token_family, enforce_token_limit_for_user, 29 + get_token_by_id, get_token_by_previous_refresh_token, get_token_by_refresh_token, 30 + list_tokens_for_user, lookup_refresh_token, revoke_tokens_for_client, 31 + revoke_tokens_for_controller, rotate_token, 31 32 }; 32 33 pub use two_factor::{ 33 34 TwoFactorChallenge, check_user_2fa_enabled, cleanup_expired_2fa_challenges,
+14 -1
src/oauth/db/request.rs
··· 1 - use super::super::{AuthorizationRequestParameters, ClientAuth, OAuthError, RequestData}; 1 + use super::super::{AuthFlowState, AuthorizationRequestParameters, ClientAuth, OAuthError, RequestData}; 2 2 use super::helpers::{from_json, to_json}; 3 3 use sqlx::PgPool; 4 + 5 + pub async fn get_authorization_request_with_state( 6 + pool: &PgPool, 7 + request_id: &str, 8 + ) -> Result<Option<(RequestData, AuthFlowState)>, OAuthError> { 9 + match get_authorization_request(pool, request_id).await? { 10 + Some(data) => { 11 + let state = AuthFlowState::from_request_data(&data); 12 + Ok(Some((data, state))) 13 + } 14 + None => Ok(None), 15 + } 16 + } 4 17 5 18 pub async fn create_authorization_request( 6 19 pool: &PgPool,
+47 -1
src/oauth/db/token.rs
··· 1 - use super::super::{OAuthError, TokenData}; 1 + use super::super::{OAuthError, RefreshTokenState, TokenData}; 2 2 use super::helpers::{from_json, to_json}; 3 3 use chrono::{DateTime, Utc}; 4 4 use sqlx::PgPool; 5 + 6 + pub enum RefreshTokenLookup { 7 + Valid { db_id: i32, token_data: TokenData }, 8 + InGracePeriod { db_id: i32, token_data: TokenData, rotated_at: DateTime<Utc> }, 9 + Used { original_token_id: i32 }, 10 + Expired { db_id: i32 }, 11 + NotFound, 12 + } 13 + 14 + impl RefreshTokenLookup { 15 + pub fn state(&self) -> RefreshTokenState { 16 + match self { 17 + RefreshTokenLookup::Valid { .. } => RefreshTokenState::Valid, 18 + RefreshTokenLookup::InGracePeriod { rotated_at, .. } => { 19 + RefreshTokenState::InGracePeriod { rotated_at: *rotated_at } 20 + } 21 + RefreshTokenLookup::Used { .. } => RefreshTokenState::Used { at: Utc::now() }, 22 + RefreshTokenLookup::Expired { .. } => RefreshTokenState::Expired, 23 + RefreshTokenLookup::NotFound => RefreshTokenState::Revoked, 24 + } 25 + } 26 + } 27 + 28 + pub async fn lookup_refresh_token( 29 + pool: &PgPool, 30 + refresh_token: &str, 31 + ) -> Result<RefreshTokenLookup, OAuthError> { 32 + if let Some(token_id) = check_refresh_token_used(pool, refresh_token).await? { 33 + if let Some((db_id, token_data)) = get_token_by_previous_refresh_token(pool, refresh_token).await? { 34 + let rotated_at = token_data.updated_at; 35 + return Ok(RefreshTokenLookup::InGracePeriod { db_id, token_data, rotated_at }); 36 + } 37 + return Ok(RefreshTokenLookup::Used { original_token_id: token_id }); 38 + } 39 + 40 + match get_token_by_refresh_token(pool, refresh_token).await? { 41 + Some((db_id, token_data)) => { 42 + if token_data.expires_at < Utc::now() { 43 + Ok(RefreshTokenLookup::Expired { db_id }) 44 + } else { 45 + Ok(RefreshTokenLookup::Valid { db_id, token_data }) 46 + } 47 + } 48 + None => Ok(RefreshTokenLookup::NotFound), 49 + } 50 + } 5 51 6 52 pub async fn create_token(pool: &PgPool, data: &TokenData) -> Result<i32, OAuthError> { 7 53 let client_auth_json = to_json(&data.client_auth)?;
+5 -4
src/oauth/dpop.rs
··· 5 5 use sha2::{Digest, Sha256}; 6 6 7 7 use super::OAuthError; 8 + use crate::types::{DPoPProofId, JwkThumbprint}; 8 9 9 10 const DPOP_NONCE_VALIDITY_SECS: i64 = 300; 10 11 const DPOP_MAX_AGE_SECS: i64 = 300; 11 12 12 13 #[derive(Debug, Clone)] 13 14 pub struct DPoPVerifyResult { 14 - pub jkt: String, 15 - pub jti: String, 15 + pub jkt: JwkThumbprint, 16 + pub jti: DPoPProofId, 16 17 } 17 18 18 19 #[derive(Debug, Clone, Serialize, Deserialize)] ··· 179 180 )?; 180 181 let jkt = compute_jwk_thumbprint(&header.jwk)?; 181 182 Ok(DPoPVerifyResult { 182 - jkt, 183 - jti: payload.jti.clone(), 183 + jkt: jkt.into(), 184 + jti: payload.jti.clone().into(), 184 185 }) 185 186 } 186 187 }
+87 -93
src/oauth/endpoints/authorize.rs
··· 1 1 use crate::comms::{CommsChannel, channel_display_name, enqueue_2fa_code}; 2 2 use crate::oauth::{ 3 - Code, DeviceData, DeviceId, OAuthError, SessionId, client::ClientMetadataCache, db, 3 + AuthFlowState, Code, DeviceData, DeviceId, OAuthError, SessionId, client::ClientMetadataCache, 4 + db, 4 5 }; 5 6 use crate::state::{AppState, RateLimitKind}; 7 + use crate::types::{Handle, PlainPassword}; 6 8 use axum::{ 7 9 Json, 8 10 extract::{Query, State}, ··· 29 31 url_encode(error), 30 32 url_encode(description) 31 33 )) 34 + } 35 + 36 + fn json_error(status: StatusCode, error: &str, description: &str) -> Response { 37 + ( 38 + status, 39 + Json(serde_json::json!({ 40 + "error": error, 41 + "error_description": description 42 + })), 43 + ) 44 + .into_response() 45 + } 46 + 47 + fn validate_auth_flow_state( 48 + flow_state: &AuthFlowState, 49 + require_authenticated: bool, 50 + ) -> Option<Response> { 51 + if flow_state.is_expired() { 52 + return Some(json_error( 53 + StatusCode::BAD_REQUEST, 54 + "invalid_request", 55 + "Authorization request has expired", 56 + )); 57 + } 58 + if require_authenticated && flow_state.is_pending() { 59 + return Some(json_error( 60 + StatusCode::FORBIDDEN, 61 + "access_denied", 62 + "Not authenticated", 63 + )); 64 + } 65 + None 32 66 } 33 67 34 68 fn extract_device_cookie(headers: &HeaderMap) -> Option<String> { ··· 97 131 pub struct AuthorizeSubmit { 98 132 pub request_uri: String, 99 133 pub username: String, 100 - pub password: String, 134 + pub password: PlainPassword, 101 135 #[serde(default)] 102 136 pub remember_device: bool, 103 137 } ··· 298 332 #[derive(Debug, Serialize)] 299 333 pub struct AccountInfo { 300 334 pub did: String, 301 - pub handle: String, 335 + pub handle: Handle, 302 336 #[serde(skip_serializing_if = "Option::is_none")] 303 337 pub email: Option<String>, 304 338 } ··· 1155 1189 State(state): State<AppState>, 1156 1190 Query(query): Query<ConsentQuery>, 1157 1191 ) -> Response { 1158 - let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 1159 - Ok(Some(data)) => data, 1160 - Ok(None) => { 1161 - return ( 1162 - StatusCode::BAD_REQUEST, 1163 - Json(serde_json::json!({ 1164 - "error": "invalid_request", 1165 - "error_description": "Invalid or expired request_uri" 1166 - })), 1167 - ) 1168 - .into_response(); 1169 - } 1170 - Err(e) => { 1171 - return ( 1172 - StatusCode::INTERNAL_SERVER_ERROR, 1173 - Json(serde_json::json!({ 1174 - "error": "server_error", 1175 - "error_description": format!("Database error: {:?}", e) 1176 - })), 1177 - ) 1178 - .into_response(); 1192 + let (request_data, flow_state) = 1193 + match db::get_authorization_request_with_state(&state.db, &query.request_uri).await { 1194 + Ok(Some(result)) => result, 1195 + Ok(None) => { 1196 + return json_error( 1197 + StatusCode::BAD_REQUEST, 1198 + "invalid_request", 1199 + "Invalid or expired request_uri", 1200 + ); 1201 + } 1202 + Err(e) => { 1203 + return json_error( 1204 + StatusCode::INTERNAL_SERVER_ERROR, 1205 + "server_error", 1206 + &format!("Database error: {:?}", e), 1207 + ); 1208 + } 1209 + }; 1210 + 1211 + if let Some(err_response) = validate_auth_flow_state(&flow_state, true) { 1212 + if flow_state.is_expired() { 1213 + let _ = db::delete_authorization_request(&state.db, &query.request_uri).await; 1179 1214 } 1180 - }; 1181 - if request_data.expires_at < Utc::now() { 1182 - let _ = db::delete_authorization_request(&state.db, &query.request_uri).await; 1183 - return ( 1184 - StatusCode::BAD_REQUEST, 1185 - Json(serde_json::json!({ 1186 - "error": "invalid_request", 1187 - "error_description": "Authorization request has expired" 1188 - })), 1189 - ) 1190 - .into_response(); 1215 + return err_response; 1191 1216 } 1192 - let did = match &request_data.did { 1193 - Some(d) => d.clone(), 1194 - None => { 1195 - return ( 1196 - StatusCode::FORBIDDEN, 1197 - Json(serde_json::json!({ 1198 - "error": "access_denied", 1199 - "error_description": "Not authenticated" 1200 - })), 1201 - ) 1202 - .into_response(); 1203 - } 1204 - }; 1217 + 1218 + let did = flow_state.did().unwrap().to_string(); 1205 1219 let client_cache = ClientMetadataCache::new(3600); 1206 1220 let client_metadata = client_cache 1207 1221 .get(&request_data.parameters.client_id) ··· 1334 1348 form.approved_scopes, 1335 1349 form.remember 1336 1350 ); 1337 - let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1338 - Ok(Some(data)) => data, 1339 - Ok(None) => { 1340 - return ( 1341 - StatusCode::BAD_REQUEST, 1342 - Json(serde_json::json!({ 1343 - "error": "invalid_request", 1344 - "error_description": "Invalid or expired request_uri" 1345 - })), 1346 - ) 1347 - .into_response(); 1348 - } 1349 - Err(e) => { 1350 - return ( 1351 - StatusCode::INTERNAL_SERVER_ERROR, 1352 - Json(serde_json::json!({ 1353 - "error": "server_error", 1354 - "error_description": format!("Database error: {:?}", e) 1355 - })), 1356 - ) 1357 - .into_response(); 1351 + let (request_data, flow_state) = 1352 + match db::get_authorization_request_with_state(&state.db, &form.request_uri).await { 1353 + Ok(Some(result)) => result, 1354 + Ok(None) => { 1355 + return json_error( 1356 + StatusCode::BAD_REQUEST, 1357 + "invalid_request", 1358 + "Invalid or expired request_uri", 1359 + ); 1360 + } 1361 + Err(e) => { 1362 + return json_error( 1363 + StatusCode::INTERNAL_SERVER_ERROR, 1364 + "server_error", 1365 + &format!("Database error: {:?}", e), 1366 + ); 1367 + } 1368 + }; 1369 + 1370 + if let Some(err_response) = validate_auth_flow_state(&flow_state, true) { 1371 + if flow_state.is_expired() { 1372 + let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1358 1373 } 1359 - }; 1360 - if request_data.expires_at < Utc::now() { 1361 - let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1362 - return ( 1363 - StatusCode::BAD_REQUEST, 1364 - Json(serde_json::json!({ 1365 - "error": "invalid_request", 1366 - "error_description": "Authorization request has expired" 1367 - })), 1368 - ) 1369 - .into_response(); 1374 + return err_response; 1370 1375 } 1371 - let did = match &request_data.did { 1372 - Some(d) => d.clone(), 1373 - None => { 1374 - return ( 1375 - StatusCode::FORBIDDEN, 1376 - Json(serde_json::json!({ 1377 - "error": "access_denied", 1378 - "error_description": "Not authenticated" 1379 - })), 1380 - ) 1381 - .into_response(); 1382 - } 1383 - }; 1376 + 1377 + let did = flow_state.did().unwrap().to_string(); 1384 1378 let original_scope_str = request_data 1385 1379 .parameters 1386 1380 .scope
+2 -1
src/oauth/endpoints/delegation.rs
··· 1 1 use crate::delegation; 2 2 use crate::oauth::db; 3 3 use crate::state::{AppState, RateLimitKind}; 4 + use crate::types::PlainPassword; 4 5 use crate::util::extract_client_ip; 5 6 use axum::{ 6 7 Json, ··· 15 16 pub request_uri: String, 16 17 pub delegated_did: Option<String>, 17 18 pub controller_did: String, 18 - pub password: String, 19 + pub password: PlainPassword, 19 20 #[serde(default)] 20 21 pub remember_device: bool, 21 22 }
+66 -60
src/oauth/endpoints/token/grants.rs
··· 1 1 use super::helpers::{create_access_token_with_delegation, verify_pkce}; 2 - use super::types::{TokenRequest, TokenResponse}; 2 + use super::types::{TokenGrant, TokenResponse, ValidatedTokenRequest}; 3 3 use crate::config::AuthConfig; 4 4 use crate::delegation; 5 5 use crate::oauth::{ 6 - ClientAuth, OAuthError, RefreshToken, TokenData, TokenId, 6 + AuthFlowState, ClientAuth, OAuthError, RefreshToken, TokenData, TokenId, 7 7 client::{ClientMetadataCache, verify_client_auth}, 8 - db, 8 + db::{self, RefreshTokenLookup}, 9 9 dpop::DPoPVerifier, 10 10 }; 11 11 use crate::state::AppState; ··· 20 20 pub async fn handle_authorization_code_grant( 21 21 state: AppState, 22 22 _headers: HeaderMap, 23 - request: TokenRequest, 23 + request: ValidatedTokenRequest, 24 24 dpop_proof: Option<String>, 25 25 ) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> { 26 - let code = request 27 - .code 28 - .ok_or_else(|| OAuthError::InvalidRequest("code is required".to_string()))?; 29 - let code_verifier = request 30 - .code_verifier 31 - .ok_or_else(|| OAuthError::InvalidRequest("code_verifier is required".to_string()))?; 26 + let (code, code_verifier, redirect_uri) = match request.grant { 27 + TokenGrant::AuthorizationCode { code, code_verifier, redirect_uri } => { 28 + (code, code_verifier, redirect_uri) 29 + } 30 + _ => return Err(OAuthError::InvalidRequest("Expected authorization_code grant".to_string())), 31 + }; 32 32 let auth_request = db::consume_authorization_request_by_code(&state.db, &code) 33 33 .await? 34 34 .ok_or_else(|| OAuthError::InvalidGrant("Invalid or expired code".to_string()))?; 35 - if auth_request.expires_at < Utc::now() { 35 + 36 + let flow_state = AuthFlowState::from_request_data(&auth_request); 37 + if flow_state.is_expired() { 36 38 return Err(OAuthError::InvalidGrant( 37 39 "Authorization code has expired".to_string(), 38 40 )); 39 41 } 40 - if let Some(request_client_id) = &request.client_id 42 + if !flow_state.can_exchange() { 43 + return Err(OAuthError::InvalidGrant( 44 + "Authorization not completed".to_string(), 45 + )); 46 + } 47 + 48 + if let Some(request_client_id) = &request.client_auth.client_id 41 49 && request_client_id != &auth_request.client_id 42 50 { 43 51 return Err(OAuthError::InvalidGrant("client_id mismatch".to_string())); 44 52 } 45 - let did = auth_request 46 - .did 47 - .ok_or_else(|| OAuthError::InvalidGrant("Authorization not completed".to_string()))?; 53 + let did = flow_state.did().unwrap().to_string(); 48 54 let client_metadata_cache = ClientMetadataCache::new(3600); 49 55 let client_metadata = client_metadata_cache.get(&auth_request.client_id).await?; 50 56 let client_auth = if let (Some(assertion), Some(assertion_type)) = 51 - (&request.client_assertion, &request.client_assertion_type) 57 + (&request.client_auth.client_assertion, &request.client_auth.client_assertion_type) 52 58 { 53 59 if assertion_type != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" { 54 60 return Err(OAuthError::InvalidClient( ··· 58 64 ClientAuth::PrivateKeyJwt { 59 65 client_assertion: assertion.clone(), 60 66 } 61 - } else if let Some(secret) = &request.client_secret { 67 + } else if let Some(secret) = &request.client_auth.client_secret { 62 68 ClientAuth::SecretPost { 63 69 client_secret: secret.clone(), 64 70 } ··· 67 73 }; 68 74 verify_client_auth(&client_metadata_cache, &client_metadata, &client_auth).await?; 69 75 verify_pkce(&auth_request.parameters.code_challenge, &code_verifier)?; 70 - if let Some(redirect_uri) = &request.redirect_uri 71 - && redirect_uri != &auth_request.parameters.redirect_uri 76 + if let Some(req_redirect_uri) = &redirect_uri 77 + && req_redirect_uri != &auth_request.parameters.redirect_uri 72 78 { 73 79 return Err(OAuthError::InvalidGrant( 74 80 "redirect_uri mismatch".to_string(), ··· 87 93 )); 88 94 } 89 95 if let Some(expected_jkt) = &auth_request.parameters.dpop_jkt 90 - && &result.jkt != expected_jkt 96 + && result.jkt.as_str() != expected_jkt 91 97 { 92 98 return Err(OAuthError::InvalidDpopProof( 93 99 "DPoP key binding mismatch".to_string(), 94 100 )); 95 101 } 96 - Some(result.jkt) 102 + Some(result.jkt.as_str().to_string()) 97 103 } else if auth_request.parameters.dpop_jkt.is_some() || client_metadata.requires_dpop() { 98 104 return Err(OAuthError::UseDpopNonce( 99 105 crate::oauth::dpop::DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes()) ··· 187 193 pub async fn handle_refresh_token_grant( 188 194 state: AppState, 189 195 _headers: HeaderMap, 190 - request: TokenRequest, 196 + request: ValidatedTokenRequest, 191 197 dpop_proof: Option<String>, 192 198 ) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> { 193 - let refresh_token_str = request 194 - .refresh_token 195 - .ok_or_else(|| OAuthError::InvalidRequest("refresh_token is required".to_string()))?; 199 + let refresh_token_str = match request.grant { 200 + TokenGrant::RefreshToken { refresh_token } => refresh_token, 201 + _ => return Err(OAuthError::InvalidRequest("Expected refresh_token grant".to_string())), 202 + }; 203 + let token_prefix = &refresh_token_str[..std::cmp::min(16, refresh_token_str.len())]; 196 204 tracing::info!( 197 - refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())], 205 + refresh_token_prefix = %token_prefix, 198 206 has_dpop = dpop_proof.is_some(), 199 207 "Refresh token grant requested" 200 208 ); 201 - if let Some(token_id) = db::check_refresh_token_used(&state.db, &refresh_token_str).await? { 202 - if let Some((_db_id, token_data)) = 203 - db::get_token_by_previous_refresh_token(&state.db, &refresh_token_str).await? 204 - { 209 + 210 + let lookup = db::lookup_refresh_token(&state.db, &refresh_token_str).await?; 211 + let token_state = lookup.state(); 212 + tracing::debug!(state = %token_state, "Refresh token state"); 213 + 214 + let (db_id, token_data) = match lookup { 215 + RefreshTokenLookup::Valid { db_id, token_data } => (db_id, token_data), 216 + RefreshTokenLookup::InGracePeriod { db_id: _, token_data, rotated_at } => { 205 217 tracing::info!( 206 - refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())], 218 + refresh_token_prefix = %token_prefix, 219 + rotated_at = %rotated_at, 207 220 "Refresh token reuse within grace period, returning existing tokens" 208 221 ); 209 222 let dpop_jkt = token_data.parameters.dpop_jkt.as_deref(); ··· 230 243 }), 231 244 )); 232 245 } 233 - tracing::warn!( 234 - refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())], 235 - "Refresh token reuse detected, revoking token family" 236 - ); 237 - db::delete_token_family(&state.db, token_id).await?; 238 - return Err(OAuthError::InvalidGrant( 239 - "Refresh token reuse detected, token family revoked".to_string(), 240 - )); 241 - } 242 - let (db_id, token_data) = db::get_token_by_refresh_token(&state.db, &refresh_token_str) 243 - .await? 244 - .ok_or_else(|| { 246 + RefreshTokenLookup::Used { original_token_id } => { 245 247 tracing::warn!( 246 - refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())], 247 - "Refresh token not found in database" 248 + refresh_token_prefix = %token_prefix, 249 + "Refresh token reuse detected, revoking token family" 248 250 ); 249 - OAuthError::InvalidGrant("Invalid refresh token".to_string()) 250 - })?; 251 - if token_data.expires_at < Utc::now() { 252 - tracing::warn!( 253 - did = %token_data.did, 254 - expired_at = %token_data.expires_at, 255 - "Refresh token has expired" 256 - ); 257 - db::delete_token_family(&state.db, db_id).await?; 258 - return Err(OAuthError::InvalidGrant( 259 - "Refresh token has expired".to_string(), 260 - )); 261 - } 251 + db::delete_token_family(&state.db, original_token_id).await?; 252 + return Err(OAuthError::InvalidGrant( 253 + "Refresh token reuse detected, token family revoked".to_string(), 254 + )); 255 + } 256 + RefreshTokenLookup::Expired { db_id } => { 257 + tracing::warn!(refresh_token_prefix = %token_prefix, "Refresh token has expired"); 258 + db::delete_token_family(&state.db, db_id).await?; 259 + return Err(OAuthError::InvalidGrant( 260 + "Refresh token has expired".to_string(), 261 + )); 262 + } 263 + RefreshTokenLookup::NotFound => { 264 + tracing::warn!(refresh_token_prefix = %token_prefix, "Refresh token not found"); 265 + return Err(OAuthError::InvalidGrant("Invalid refresh token".to_string())); 266 + } 267 + }; 262 268 let dpop_jkt = if let Some(proof) = &dpop_proof { 263 269 let config = AuthConfig::get(); 264 270 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes()); ··· 272 278 )); 273 279 } 274 280 if let Some(expected_jkt) = &token_data.parameters.dpop_jkt 275 - && &result.jkt != expected_jkt 281 + && result.jkt.as_str() != expected_jkt 276 282 { 277 283 return Err(OAuthError::InvalidDpopProof( 278 284 "DPoP key binding mismatch".to_string(), 279 285 )); 280 286 } 281 - Some(result.jkt) 287 + Some(result.jkt.as_str().to_string()) 282 288 } else if token_data.parameters.dpop_jkt.is_some() { 283 289 return Err(OAuthError::InvalidRequest( 284 290 "DPoP proof required".to_string(),
+8 -9
src/oauth/endpoints/token/mod.rs
··· 13 13 pub use introspect::{ 14 14 IntrospectRequest, IntrospectResponse, RevokeRequest, introspect_token, revoke_token, 15 15 }; 16 - pub use types::{TokenRequest, TokenResponse}; 16 + pub use types::{ClientAuthParams, GrantType, TokenGrant, TokenRequest, TokenResponse, ValidatedTokenRequest}; 17 17 18 18 fn extract_client_ip(headers: &HeaderMap) -> String { 19 19 if let Some(forwarded) = headers.get("x-forwarded-for") ··· 65 65 .get("DPoP") 66 66 .and_then(|v| v.to_str().ok()) 67 67 .map(|s| s.to_string()); 68 - match request.grant_type.as_str() { 69 - "authorization_code" => { 70 - handle_authorization_code_grant(state, headers, request, dpop_proof).await 68 + let validated = request.validate()?; 69 + match validated.grant { 70 + TokenGrant::AuthorizationCode { .. } => { 71 + handle_authorization_code_grant(state, headers, validated, dpop_proof).await 72 + } 73 + TokenGrant::RefreshToken { .. } => { 74 + handle_refresh_token_grant(state, headers, validated, dpop_proof).await 71 75 } 72 - "refresh_token" => handle_refresh_token_grant(state, headers, request, dpop_proof).await, 73 - _ => Err(OAuthError::UnsupportedGrantType(format!( 74 - "Unsupported grant_type: {}", 75 - request.grant_type 76 - ))), 77 76 } 78 77 }
+114 -1
src/oauth/endpoints/token/types.rs
··· 1 + use crate::oauth::OAuthError; 1 2 use serde::{Deserialize, Serialize}; 2 3 4 + #[derive(Debug, Clone, PartialEq, Eq)] 5 + pub enum GrantType { 6 + AuthorizationCode, 7 + RefreshToken, 8 + Unsupported(String), 9 + } 10 + 11 + impl GrantType { 12 + pub fn as_str(&self) -> &str { 13 + match self { 14 + Self::AuthorizationCode => "authorization_code", 15 + Self::RefreshToken => "refresh_token", 16 + Self::Unsupported(s) => s, 17 + } 18 + } 19 + } 20 + 21 + impl std::str::FromStr for GrantType { 22 + type Err = std::convert::Infallible; 23 + 24 + fn from_str(s: &str) -> Result<Self, Self::Err> { 25 + Ok(match s { 26 + "authorization_code" => Self::AuthorizationCode, 27 + "refresh_token" => Self::RefreshToken, 28 + other => Self::Unsupported(other.to_string()), 29 + }) 30 + } 31 + } 32 + 33 + impl<'de> Deserialize<'de> for GrantType { 34 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 35 + where 36 + D: serde::Deserializer<'de>, 37 + { 38 + let s = String::deserialize(deserializer)?; 39 + Ok(s.parse().unwrap()) 40 + } 41 + } 42 + 43 + impl Serialize for GrantType { 44 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 45 + where 46 + S: serde::Serializer, 47 + { 48 + serializer.serialize_str(self.as_str()) 49 + } 50 + } 51 + 3 52 #[derive(Debug, Deserialize)] 4 53 pub struct TokenRequest { 5 - pub grant_type: String, 54 + pub grant_type: GrantType, 6 55 #[serde(default)] 7 56 pub code: Option<String>, 8 57 #[serde(default)] ··· 19 68 pub client_assertion: Option<String>, 20 69 #[serde(default)] 21 70 pub client_assertion_type: Option<String>, 71 + } 72 + 73 + #[derive(Debug, Clone)] 74 + pub enum TokenGrant { 75 + AuthorizationCode { 76 + code: String, 77 + code_verifier: String, 78 + redirect_uri: Option<String>, 79 + }, 80 + RefreshToken { 81 + refresh_token: String, 82 + }, 83 + } 84 + 85 + #[derive(Debug, Clone, Default)] 86 + pub struct ClientAuthParams { 87 + pub client_id: Option<String>, 88 + pub client_secret: Option<String>, 89 + pub client_assertion: Option<String>, 90 + pub client_assertion_type: Option<String>, 91 + } 92 + 93 + #[derive(Debug, Clone)] 94 + pub struct ValidatedTokenRequest { 95 + pub grant: TokenGrant, 96 + pub client_auth: ClientAuthParams, 97 + } 98 + 99 + impl TokenRequest { 100 + pub fn validate(self) -> Result<ValidatedTokenRequest, OAuthError> { 101 + let grant = match self.grant_type { 102 + GrantType::AuthorizationCode => { 103 + let code = self.code.ok_or_else(|| { 104 + OAuthError::InvalidRequest("code is required for authorization_code grant".to_string()) 105 + })?; 106 + let code_verifier = self.code_verifier.ok_or_else(|| { 107 + OAuthError::InvalidRequest("code_verifier is required for authorization_code grant".to_string()) 108 + })?; 109 + TokenGrant::AuthorizationCode { 110 + code, 111 + code_verifier, 112 + redirect_uri: self.redirect_uri, 113 + } 114 + } 115 + GrantType::RefreshToken => { 116 + let refresh_token = self.refresh_token.ok_or_else(|| { 117 + OAuthError::InvalidRequest("refresh_token is required for refresh_token grant".to_string()) 118 + })?; 119 + TokenGrant::RefreshToken { refresh_token } 120 + } 121 + GrantType::Unsupported(grant_type) => { 122 + return Err(OAuthError::UnsupportedGrantType(grant_type)); 123 + } 124 + }; 125 + 126 + let client_auth = ClientAuthParams { 127 + client_id: self.client_id, 128 + client_secret: self.client_secret, 129 + client_assertion: self.client_assertion, 130 + client_assertion_type: self.client_assertion_type, 131 + }; 132 + 133 + Ok(ValidatedTokenRequest { grant, client_auth }) 134 + } 22 135 } 23 136 24 137 #[derive(Debug, Serialize)]
+7 -10
src/oauth/scopes/parser.rs
··· 140 140 } 141 141 142 142 fn parse_query_params(query: &str) -> HashMap<String, Vec<String>> { 143 - let mut params: HashMap<String, Vec<String>> = HashMap::new(); 144 - for part in query.split('&') { 145 - if let Some((key, value)) = part.split_once('=') { 146 - params 147 - .entry(key.to_string()) 148 - .or_default() 149 - .push(value.to_string()); 150 - } 151 - } 152 - params 143 + query 144 + .split('&') 145 + .filter_map(|part| part.split_once('=')) 146 + .fold(HashMap::new(), |mut acc, (key, value)| { 147 + acc.entry(key.to_string()).or_default().push(value.to_string()); 148 + acc 149 + }) 153 150 } 154 151 155 152 pub fn parse_scope(scope: &str) -> ParsedScope {
+279
src/oauth/types.rs
··· 245 245 pub struct Jwks { 246 246 pub keys: Vec<JwkPublicKey>, 247 247 } 248 + 249 + #[derive(Debug, Clone, PartialEq, Eq)] 250 + pub enum AuthFlowState { 251 + Pending, 252 + Authenticated { did: String, device_id: Option<String> }, 253 + Authorized { did: String, device_id: Option<String>, code: String }, 254 + Expired, 255 + } 256 + 257 + impl AuthFlowState { 258 + pub fn from_request_data(data: &RequestData) -> Self { 259 + if data.expires_at < chrono::Utc::now() { 260 + return AuthFlowState::Expired; 261 + } 262 + match (&data.did, &data.code) { 263 + (Some(did), Some(code)) => AuthFlowState::Authorized { 264 + did: did.clone(), 265 + device_id: data.device_id.clone(), 266 + code: code.clone(), 267 + }, 268 + (Some(did), None) => AuthFlowState::Authenticated { 269 + did: did.clone(), 270 + device_id: data.device_id.clone(), 271 + }, 272 + (None, _) => AuthFlowState::Pending, 273 + } 274 + } 275 + 276 + pub fn is_pending(&self) -> bool { 277 + matches!(self, AuthFlowState::Pending) 278 + } 279 + 280 + pub fn is_authenticated(&self) -> bool { 281 + matches!(self, AuthFlowState::Authenticated { .. }) 282 + } 283 + 284 + pub fn is_authorized(&self) -> bool { 285 + matches!(self, AuthFlowState::Authorized { .. }) 286 + } 287 + 288 + pub fn is_expired(&self) -> bool { 289 + matches!(self, AuthFlowState::Expired) 290 + } 291 + 292 + pub fn can_authenticate(&self) -> bool { 293 + matches!(self, AuthFlowState::Pending) 294 + } 295 + 296 + pub fn can_authorize(&self) -> bool { 297 + matches!(self, AuthFlowState::Authenticated { .. }) 298 + } 299 + 300 + pub fn can_exchange(&self) -> bool { 301 + matches!(self, AuthFlowState::Authorized { .. }) 302 + } 303 + 304 + pub fn did(&self) -> Option<&str> { 305 + match self { 306 + AuthFlowState::Authenticated { did, .. } | AuthFlowState::Authorized { did, .. } => { 307 + Some(did) 308 + } 309 + _ => None, 310 + } 311 + } 312 + 313 + pub fn code(&self) -> Option<&str> { 314 + match self { 315 + AuthFlowState::Authorized { code, .. } => Some(code), 316 + _ => None, 317 + } 318 + } 319 + } 320 + 321 + impl std::fmt::Display for AuthFlowState { 322 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 323 + match self { 324 + AuthFlowState::Pending => write!(f, "pending"), 325 + AuthFlowState::Authenticated { did, .. } => write!(f, "authenticated ({})", did), 326 + AuthFlowState::Authorized { did, code, .. } => { 327 + write!(f, "authorized ({}, code={}...)", did, &code[..8.min(code.len())]) 328 + } 329 + AuthFlowState::Expired => write!(f, "expired"), 330 + } 331 + } 332 + } 333 + 334 + #[derive(Debug, Clone, PartialEq, Eq)] 335 + pub enum RefreshTokenState { 336 + Valid, 337 + Used { at: chrono::DateTime<chrono::Utc> }, 338 + InGracePeriod { rotated_at: chrono::DateTime<chrono::Utc> }, 339 + Expired, 340 + Revoked, 341 + } 342 + 343 + impl RefreshTokenState { 344 + pub fn is_valid(&self) -> bool { 345 + matches!(self, RefreshTokenState::Valid) 346 + } 347 + 348 + pub fn is_usable(&self) -> bool { 349 + matches!( 350 + self, 351 + RefreshTokenState::Valid | RefreshTokenState::InGracePeriod { .. } 352 + ) 353 + } 354 + 355 + pub fn is_used(&self) -> bool { 356 + matches!(self, RefreshTokenState::Used { .. }) 357 + } 358 + 359 + pub fn is_in_grace_period(&self) -> bool { 360 + matches!(self, RefreshTokenState::InGracePeriod { .. }) 361 + } 362 + 363 + pub fn is_expired(&self) -> bool { 364 + matches!(self, RefreshTokenState::Expired) 365 + } 366 + 367 + pub fn is_revoked(&self) -> bool { 368 + matches!(self, RefreshTokenState::Revoked) 369 + } 370 + } 371 + 372 + impl std::fmt::Display for RefreshTokenState { 373 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 374 + match self { 375 + RefreshTokenState::Valid => write!(f, "valid"), 376 + RefreshTokenState::Used { at } => write!(f, "used ({})", at), 377 + RefreshTokenState::InGracePeriod { rotated_at } => { 378 + write!(f, "grace period (rotated {})", rotated_at) 379 + } 380 + RefreshTokenState::Expired => write!(f, "expired"), 381 + RefreshTokenState::Revoked => write!(f, "revoked"), 382 + } 383 + } 384 + } 385 + 386 + #[cfg(test)] 387 + mod tests { 388 + use super::*; 389 + use chrono::{Duration, Utc}; 390 + 391 + fn make_request_data( 392 + did: Option<String>, 393 + code: Option<String>, 394 + expires_in: Duration, 395 + ) -> RequestData { 396 + RequestData { 397 + client_id: "test-client".into(), 398 + client_auth: None, 399 + parameters: AuthorizationRequestParameters { 400 + response_type: "code".into(), 401 + client_id: "test-client".into(), 402 + redirect_uri: "https://example.com/callback".into(), 403 + scope: Some("atproto".into()), 404 + state: None, 405 + code_challenge: "test".into(), 406 + code_challenge_method: "S256".into(), 407 + response_mode: None, 408 + login_hint: None, 409 + dpop_jkt: None, 410 + extra: None, 411 + }, 412 + expires_at: Utc::now() + expires_in, 413 + did, 414 + device_id: None, 415 + code, 416 + controller_did: None, 417 + } 418 + } 419 + 420 + #[test] 421 + fn test_auth_flow_state_pending() { 422 + let data = make_request_data(None, None, Duration::minutes(5)); 423 + let state = AuthFlowState::from_request_data(&data); 424 + assert!(state.is_pending()); 425 + assert!(!state.is_authenticated()); 426 + assert!(!state.is_authorized()); 427 + assert!(!state.is_expired()); 428 + assert!(state.can_authenticate()); 429 + assert!(!state.can_authorize()); 430 + assert!(!state.can_exchange()); 431 + assert!(state.did().is_none()); 432 + assert!(state.code().is_none()); 433 + } 434 + 435 + #[test] 436 + fn test_auth_flow_state_authenticated() { 437 + let data = make_request_data(Some("did:plc:test".into()), None, Duration::minutes(5)); 438 + let state = AuthFlowState::from_request_data(&data); 439 + assert!(!state.is_pending()); 440 + assert!(state.is_authenticated()); 441 + assert!(!state.is_authorized()); 442 + assert!(!state.is_expired()); 443 + assert!(!state.can_authenticate()); 444 + assert!(state.can_authorize()); 445 + assert!(!state.can_exchange()); 446 + assert_eq!(state.did(), Some("did:plc:test")); 447 + assert!(state.code().is_none()); 448 + } 449 + 450 + #[test] 451 + fn test_auth_flow_state_authorized() { 452 + let data = make_request_data( 453 + Some("did:plc:test".into()), 454 + Some("auth-code-123".into()), 455 + Duration::minutes(5), 456 + ); 457 + let state = AuthFlowState::from_request_data(&data); 458 + assert!(!state.is_pending()); 459 + assert!(!state.is_authenticated()); 460 + assert!(state.is_authorized()); 461 + assert!(!state.is_expired()); 462 + assert!(!state.can_authenticate()); 463 + assert!(!state.can_authorize()); 464 + assert!(state.can_exchange()); 465 + assert_eq!(state.did(), Some("did:plc:test")); 466 + assert_eq!(state.code(), Some("auth-code-123")); 467 + } 468 + 469 + #[test] 470 + fn test_auth_flow_state_expired() { 471 + let data = make_request_data( 472 + Some("did:plc:test".into()), 473 + Some("code".into()), 474 + Duration::minutes(-1), 475 + ); 476 + let state = AuthFlowState::from_request_data(&data); 477 + assert!(state.is_expired()); 478 + assert!(!state.can_authenticate()); 479 + assert!(!state.can_authorize()); 480 + assert!(!state.can_exchange()); 481 + } 482 + 483 + #[test] 484 + fn test_refresh_token_state_valid() { 485 + let state = RefreshTokenState::Valid; 486 + assert!(state.is_valid()); 487 + assert!(state.is_usable()); 488 + assert!(!state.is_used()); 489 + assert!(!state.is_in_grace_period()); 490 + assert!(!state.is_expired()); 491 + assert!(!state.is_revoked()); 492 + } 493 + 494 + #[test] 495 + fn test_refresh_token_state_grace_period() { 496 + let state = RefreshTokenState::InGracePeriod { 497 + rotated_at: Utc::now(), 498 + }; 499 + assert!(!state.is_valid()); 500 + assert!(state.is_usable()); 501 + assert!(!state.is_used()); 502 + assert!(state.is_in_grace_period()); 503 + } 504 + 505 + #[test] 506 + fn test_refresh_token_state_used() { 507 + let state = RefreshTokenState::Used { at: Utc::now() }; 508 + assert!(!state.is_valid()); 509 + assert!(!state.is_usable()); 510 + assert!(state.is_used()); 511 + } 512 + 513 + #[test] 514 + fn test_refresh_token_state_expired() { 515 + let state = RefreshTokenState::Expired; 516 + assert!(!state.is_usable()); 517 + assert!(state.is_expired()); 518 + } 519 + 520 + #[test] 521 + fn test_refresh_token_state_revoked() { 522 + let state = RefreshTokenState::Revoked; 523 + assert!(!state.is_usable()); 524 + assert!(state.is_revoked()); 525 + } 526 + }
+2 -2
src/oauth/verify.rs
··· 79 79 "DPoP proof has already been used".to_string(), 80 80 )); 81 81 } 82 - if &result.jkt != expected_jkt { 82 + if result.jkt.as_str() != expected_jkt { 83 83 return Err(OAuthError::InvalidDpopProof( 84 84 "DPoP key binding mismatch".to_string(), 85 85 )); ··· 374 374 375 375 async fn try_legacy_auth(pool: &PgPool, token: &str) -> Result<LegacyAuthResult, ()> { 376 376 match crate::auth::validate_bearer_token(pool, token).await { 377 - Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }), 377 + Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did.to_string() }), 378 378 _ => Err(()), 379 379 } 380 380 }
+3 -3
src/scheduled.rs
··· 431 431 } 432 432 } 433 433 _ = ticker.tick() => { 434 - if let Err(e) = process_scheduled_deletions(&db, &blob_store).await { 434 + if let Err(e) = process_scheduled_deletions(&db, blob_store.as_ref()).await { 435 435 error!("Error processing scheduled deletions: {}", e); 436 436 } 437 437 } ··· 441 441 442 442 async fn process_scheduled_deletions( 443 443 db: &PgPool, 444 - blob_store: &Arc<dyn BlobStorage>, 444 + blob_store: &dyn BlobStorage, 445 445 ) -> Result<(), String> { 446 446 let accounts_to_delete = sqlx::query!( 447 447 r#" ··· 489 489 490 490 async fn delete_account_data( 491 491 db: &PgPool, 492 - blob_store: &Arc<dyn BlobStorage>, 492 + blob_store: &dyn BlobStorage, 493 493 did: &str, 494 494 _handle: &str, 495 495 ) -> Result<(), String> {
+8 -36
src/sync/blob.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 2 3 use crate::sync::util::assert_repo_availability; 3 4 use axum::{ ··· 9 10 response::{IntoResponse, Response}, 10 11 }; 11 12 use serde::{Deserialize, Serialize}; 12 - use serde_json::json; 13 13 use tracing::error; 14 14 15 15 #[derive(Deserialize)] ··· 25 25 let did = params.did.trim(); 26 26 let cid = params.cid.trim(); 27 27 if did.is_empty() { 28 - return ( 29 - StatusCode::BAD_REQUEST, 30 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 31 - ) 32 - .into_response(); 28 + return ApiError::InvalidRequest("did is required".into()).into_response(); 33 29 } 34 30 if cid.is_empty() { 35 - return ( 36 - StatusCode::BAD_REQUEST, 37 - Json(json!({"error": "InvalidRequest", "message": "cid is required"})), 38 - ) 39 - .into_response(); 31 + return ApiError::InvalidRequest("cid is required".into()).into_response(); 40 32 } 41 33 42 34 let _account = match assert_repo_availability(&state.db, did, false).await { ··· 66 58 .unwrap(), 67 59 Err(e) => { 68 60 error!("Failed to fetch blob from storage: {:?}", e); 69 - ( 70 - StatusCode::NOT_FOUND, 71 - Json(json!({"error": "BlobNotFound", "message": "Blob not found in storage"})), 72 - ) 73 - .into_response() 61 + ApiError::BlobNotFound(Some("Blob not found in storage".into())).into_response() 74 62 } 75 63 } 76 64 } 77 - Ok(None) => ( 78 - StatusCode::NOT_FOUND, 79 - Json(json!({"error": "BlobNotFound", "message": "Blob not found"})), 80 - ) 81 - .into_response(), 65 + Ok(None) => ApiError::BlobNotFound(Some("Blob not found".into())).into_response(), 82 66 Err(e) => { 83 67 error!("DB error in get_blob: {:?}", e); 84 - ( 85 - StatusCode::INTERNAL_SERVER_ERROR, 86 - Json(json!({"error": "InternalError"})), 87 - ) 88 - .into_response() 68 + ApiError::InternalError(Some("Database error".into())).into_response() 89 69 } 90 70 } 91 71 } ··· 111 91 ) -> Response { 112 92 let did = params.did.trim(); 113 93 if did.is_empty() { 114 - return ( 115 - StatusCode::BAD_REQUEST, 116 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 117 - ) 118 - .into_response(); 94 + return ApiError::InvalidRequest("did is required".into()).into_response(); 119 95 } 120 96 121 97 let account = match assert_repo_availability(&state.db, did, false).await { ··· 178 154 } 179 155 Err(e) => { 180 156 error!("DB error in list_blobs: {:?}", e); 181 - ( 182 - StatusCode::INTERNAL_SERVER_ERROR, 183 - Json(json!({"error": "InternalError"})), 184 - ) 185 - .into_response() 157 + ApiError::InternalError(Some("Database error".into())).into_response() 186 158 } 187 159 } 188 160 }
+14 -47
src/sync/commit.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 2 3 use crate::sync::util::{AccountStatus, assert_repo_availability, get_account_with_status}; 3 4 use axum::{ ··· 10 11 use jacquard_repo::commit::Commit; 11 12 use jacquard_repo::storage::BlockStore; 12 13 use serde::{Deserialize, Serialize}; 13 - use serde_json::json; 14 14 use std::str::FromStr; 15 15 use tracing::error; 16 16 ··· 38 38 ) -> Response { 39 39 let did = params.did.trim(); 40 40 if did.is_empty() { 41 - return ( 42 - StatusCode::BAD_REQUEST, 43 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 44 - ) 45 - .into_response(); 41 + return ApiError::InvalidRequest("did is required".into()).into_response(); 46 42 } 47 43 48 44 let account = match assert_repo_availability(&state.db, did, false).await { ··· 50 46 Err(e) => return e.into_response(), 51 47 }; 52 48 53 - let repo_root_cid = match account.repo_root_cid { 54 - Some(cid) => cid, 55 - None => { 56 - return ( 57 - StatusCode::BAD_REQUEST, 58 - Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 59 - ) 60 - .into_response(); 61 - } 49 + let Some(repo_root_cid) = account.repo_root_cid else { 50 + return ApiError::RepoNotFound(Some("Repo not initialized".into())).into_response(); 62 51 }; 63 52 64 - let rev = match get_rev_from_commit(&state, &repo_root_cid).await { 65 - Some(r) => r, 66 - None => { 67 - error!( 68 - "Failed to parse commit for DID {}: CID {}", 69 - did, repo_root_cid 70 - ); 71 - return ( 72 - StatusCode::INTERNAL_SERVER_ERROR, 73 - Json(json!({"error": "InternalError", "message": "Failed to read repo commit"})), 74 - ) 75 - .into_response(); 76 - } 53 + let Some(rev) = get_rev_from_commit(&state, &repo_root_cid).await else { 54 + error!( 55 + "Failed to parse commit for DID {}: CID {}", 56 + did, repo_root_cid 57 + ); 58 + return ApiError::InternalError(Some("Failed to read repo commit".into())).into_response(); 77 59 }; 78 60 79 61 ( ··· 181 163 } 182 164 Err(e) => { 183 165 error!("DB error in list_repos: {:?}", e); 184 - ( 185 - StatusCode::INTERNAL_SERVER_ERROR, 186 - Json(json!({"error": "InternalError"})), 187 - ) 188 - .into_response() 166 + ApiError::InternalError(Some("Database error".into())).into_response() 189 167 } 190 168 } 191 169 } ··· 211 189 ) -> Response { 212 190 let did = params.did.trim(); 213 191 if did.is_empty() { 214 - return ( 215 - StatusCode::BAD_REQUEST, 216 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 217 - ) 218 - .into_response(); 192 + return ApiError::InvalidRequest("did is required".into()).into_response(); 219 193 } 220 194 221 195 let account = match get_account_with_status(&state.db, did).await { 222 196 Ok(Some(a)) => a, 223 197 Ok(None) => { 224 - return ( 225 - StatusCode::BAD_REQUEST, 226 - Json(json!({"error": "RepoNotFound", "message": format!("Could not find repo for DID: {}", did)})), 227 - ) 198 + return ApiError::RepoNotFound(Some(format!("Could not find repo for DID: {}", did))) 228 199 .into_response() 229 200 } 230 201 Err(e) => { 231 202 error!("DB error in get_repo_status: {:?}", e); 232 - return ( 233 - StatusCode::INTERNAL_SERVER_ERROR, 234 - Json(json!({"error": "InternalError"})), 235 - ) 236 - .into_response(); 203 + return ApiError::InternalError(Some("Database error".into())).into_response(); 237 204 } 238 205 }; 239 206
+3 -4
src/sync/crawl.rs
··· 1 + use crate::api::EmptyResponse; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, 4 5 extract::{Query, State}, 5 - http::StatusCode, 6 6 response::{IntoResponse, Response}, 7 7 }; 8 8 use serde::Deserialize; 9 - use serde_json::json; 10 9 use tracing::info; 11 10 12 11 #[derive(Deserialize)] ··· 19 18 Query(params): Query<NotifyOfUpdateParams>, 20 19 ) -> Response { 21 20 info!("Received notifyOfUpdate from hostname: {}", params.hostname); 22 - (StatusCode::OK, Json(json!({}))).into_response() 21 + EmptyResponse::ok().into_response() 23 22 } 24 23 25 24 #[derive(Deserialize)] ··· 32 31 Json(input): Json<RequestCrawlInput>, 33 32 ) -> Response { 34 33 info!("Received requestCrawl for hostname: {}", input.hostname); 35 - (StatusCode::OK, Json(json!({}))).into_response() 34 + EmptyResponse::ok().into_response() 36 35 }
+13 -43
src/sync/deprecated.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::auth::{extract_bearer_token_from_header, validate_bearer_token_allow_takendown}; 2 3 use crate::state::AppState; 3 4 use crate::sync::car::encode_car_header; ··· 12 13 use ipld_core::ipld::Ipld; 13 14 use jacquard_repo::storage::BlockStore; 14 15 use serde::{Deserialize, Serialize}; 15 - use serde_json::json; 16 16 use std::io::Write; 17 17 use std::str::FromStr; 18 18 ··· 48 48 ) -> Response { 49 49 let did = params.did.trim(); 50 50 if did.is_empty() { 51 - return ( 52 - StatusCode::BAD_REQUEST, 53 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 54 - ) 55 - .into_response(); 51 + return ApiError::InvalidRequest("did is required".into()).into_response(); 56 52 } 57 53 let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; 58 54 let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { ··· 61 57 }; 62 58 match account.repo_root_cid { 63 59 Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(), 64 - None => ( 65 - StatusCode::BAD_REQUEST, 66 - Json(json!({"error": "HeadNotFound", "message": format!("Could not find root for DID: {}", did)})), 67 - ) 68 - .into_response(), 60 + None => { 61 + ApiError::RepoNotFound(Some(format!("Could not find root for DID: {}", did))) 62 + .into_response() 63 + } 69 64 } 70 65 } 71 66 ··· 81 76 ) -> Response { 82 77 let did = params.did.trim(); 83 78 if did.is_empty() { 84 - return ( 85 - StatusCode::BAD_REQUEST, 86 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 87 - ) 88 - .into_response(); 79 + return ApiError::InvalidRequest("did is required".into()).into_response(); 89 80 } 90 81 let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; 91 82 let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { 92 83 Ok(a) => a, 93 84 Err(e) => return e.into_response(), 94 85 }; 95 - let head_str = match account.repo_root_cid { 96 - Some(r) => r, 97 - None => { 98 - return ( 99 - StatusCode::BAD_REQUEST, 100 - Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 101 - ) 102 - .into_response(); 103 - } 86 + let Some(head_str) = account.repo_root_cid else { 87 + return ApiError::RepoNotFound(Some("Repo not initialized".into())).into_response(); 104 88 }; 105 - let head_cid = match Cid::from_str(&head_str) { 106 - Ok(c) => c, 107 - Err(_) => { 108 - return ( 109 - StatusCode::INTERNAL_SERVER_ERROR, 110 - Json(json!({"error": "InternalError", "message": "Invalid head CID"})), 111 - ) 112 - .into_response(); 113 - } 89 + let Ok(head_cid) = Cid::from_str(&head_str) else { 90 + return ApiError::InternalError(Some("Invalid head CID".into())).into_response(); 114 91 }; 115 - let mut car_bytes = match encode_car_header(&head_cid) { 116 - Ok(h) => h, 117 - Err(e) => { 118 - return ( 119 - StatusCode::INTERNAL_SERVER_ERROR, 120 - Json(json!({"error": "InternalError", "message": format!("Failed to encode CAR header: {}", e)})), 121 - ) 122 - .into_response(); 123 - } 92 + let Ok(mut car_bytes) = encode_car_header(&head_cid) else { 93 + return ApiError::InternalError(Some("Failed to encode CAR header".into())).into_response(); 124 94 }; 125 95 let mut stack = vec![head_cid]; 126 96 let mut visited = std::collections::HashSet::new();
+78 -32
src/sync/frame.rs
··· 93 93 pub message: Option<String>, 94 94 } 95 95 96 + #[derive(Debug, Clone)] 97 + pub enum CommitFrameError { 98 + InvalidCommitCid(String), 99 + InvalidBlobCid(String), 100 + } 101 + 102 + impl std::fmt::Display for CommitFrameError { 103 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 104 + match self { 105 + Self::InvalidCommitCid(s) => write!(f, "Invalid commit CID: {}", s), 106 + Self::InvalidBlobCid(s) => write!(f, "Invalid blob CID: {}", s), 107 + } 108 + } 109 + } 110 + 111 + impl std::error::Error for CommitFrameError {} 112 + 96 113 pub struct CommitFrameBuilder { 97 - pub seq: i64, 98 - pub did: String, 99 - pub commit_cid_str: String, 100 - pub prev_cid_str: Option<String>, 101 - pub ops_json: serde_json::Value, 102 - pub blobs: Vec<String>, 103 - pub time: chrono::DateTime<chrono::Utc>, 104 - pub rev: Option<String>, 114 + seq: i64, 115 + did: String, 116 + commit_cid: Cid, 117 + prev_cid: Option<Cid>, 118 + ops_json: serde_json::Value, 119 + blob_cids: Vec<Cid>, 120 + time: chrono::DateTime<chrono::Utc>, 121 + rev: Option<String>, 105 122 } 106 123 107 124 impl CommitFrameBuilder { 108 - pub fn build(self) -> Result<CommitFrame, &'static str> { 109 - let commit_cid = Cid::from_str(&self.commit_cid_str).map_err(|_| "Invalid commit CID")?; 125 + pub fn new( 126 + seq: i64, 127 + did: String, 128 + commit_cid_str: &str, 129 + prev_cid_str: Option<&str>, 130 + ops_json: serde_json::Value, 131 + blob_strs: Vec<String>, 132 + time: chrono::DateTime<chrono::Utc>, 133 + rev: Option<String>, 134 + ) -> Result<Self, CommitFrameError> { 135 + let commit_cid = Cid::from_str(commit_cid_str) 136 + .map_err(|_| CommitFrameError::InvalidCommitCid(commit_cid_str.to_string()))?; 137 + let prev_cid = prev_cid_str 138 + .map(|s| Cid::from_str(s)) 139 + .transpose() 140 + .map_err(|_| CommitFrameError::InvalidCommitCid(prev_cid_str.unwrap_or("").to_string()))?; 141 + let blob_cids: Vec<Cid> = blob_strs 142 + .iter() 143 + .filter_map(|s| Cid::from_str(s).ok()) 144 + .collect(); 145 + Ok(Self { 146 + seq, 147 + did, 148 + commit_cid, 149 + prev_cid, 150 + ops_json, 151 + blob_cids, 152 + time, 153 + rev, 154 + }) 155 + } 156 + 157 + pub fn build(self) -> CommitFrame { 110 158 let json_ops: Vec<JsonRepoOp> = 111 159 serde_json::from_value(self.ops_json).unwrap_or_else(|_| vec![]); 112 160 let ops: Vec<RepoOp> = json_ops ··· 118 166 prev: op.prev.and_then(|s| Cid::from_str(&s).ok()), 119 167 }) 120 168 .collect(); 121 - let blobs: Vec<Cid> = self 122 - .blobs 123 - .iter() 124 - .filter_map(|s| Cid::from_str(s).ok()) 125 - .collect(); 126 169 let rev = self.rev.unwrap_or_else(placeholder_rev); 127 - let since = self.prev_cid_str.as_ref().map(|_| rev.clone()); 128 - Ok(CommitFrame { 170 + let since = self.prev_cid.as_ref().map(|_| rev.clone()); 171 + CommitFrame { 129 172 seq: self.seq, 130 173 rebase: false, 131 174 too_big: false, 132 175 repo: self.did, 133 - commit: commit_cid, 176 + commit: self.commit_cid, 134 177 rev, 135 178 since, 136 179 blocks: Vec::new(), 137 180 ops, 138 - blobs, 181 + blobs: self.blob_cids, 139 182 time: format_atproto_time(self.time), 140 183 prev_data: None, 141 - }) 184 + } 142 185 } 143 186 } 144 187 ··· 152 195 } 153 196 154 197 impl TryFrom<SequencedEvent> for CommitFrame { 155 - type Error = &'static str; 198 + type Error = CommitFrameError; 156 199 157 200 fn try_from(event: SequencedEvent) -> Result<Self, Self::Error> { 158 - let builder = CommitFrameBuilder { 159 - seq: event.seq, 160 - did: event.did, 161 - commit_cid_str: event.commit_cid.ok_or("Missing commit_cid in event")?, 162 - prev_cid_str: event.prev_cid, 163 - ops_json: event.ops.unwrap_or_default(), 164 - blobs: event.blobs.unwrap_or_default(), 165 - time: event.created_at, 166 - rev: event.rev, 167 - }; 168 - builder.build() 201 + let commit_cid_str = event.commit_cid.ok_or_else(|| { 202 + CommitFrameError::InvalidCommitCid("Missing commit_cid in event".to_string()) 203 + })?; 204 + let builder = CommitFrameBuilder::new( 205 + event.seq, 206 + event.did, 207 + &commit_cid_str, 208 + event.prev_cid.as_deref(), 209 + event.ops.unwrap_or_default(), 210 + event.blobs.unwrap_or_default(), 211 + event.created_at, 212 + event.rev, 213 + )?; 214 + Ok(builder.build()) 169 215 } 170 216 }
+47 -149
src/sync/repo.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 2 3 use crate::sync::car::encode_car_header; 3 4 use crate::sync::util::assert_repo_availability; 4 5 use axum::{ 5 - Json, 6 6 extract::{Query, RawQuery, State}, 7 7 http::StatusCode, 8 8 response::{IntoResponse, Response}, ··· 11 11 use ipld_core::ipld::Ipld; 12 12 use jacquard_repo::storage::BlockStore; 13 13 use serde::Deserialize; 14 - use serde_json::json; 15 14 use std::io::Write; 16 15 use std::str::FromStr; 17 16 use tracing::error; ··· 28 27 } 29 28 30 29 pub async fn get_blocks(State(state): State<AppState>, RawQuery(query): RawQuery) -> Response { 31 - let query_string = match query { 32 - Some(q) => q, 33 - None => { 34 - return ( 35 - StatusCode::BAD_REQUEST, 36 - Json(json!({"error": "InvalidRequest", "message": "Missing query parameters"})), 37 - ) 38 - .into_response(); 39 - } 30 + let Some(query_string) = query else { 31 + return ApiError::InvalidRequest("Missing query parameters".into()).into_response(); 40 32 }; 41 33 42 34 let (did, cid_strings) = match parse_get_blocks_query(&query_string) { 43 35 Ok(parsed) => parsed, 44 - Err(msg) => { 45 - return ( 46 - StatusCode::BAD_REQUEST, 47 - Json(json!({"error": "InvalidRequest", "message": msg})), 48 - ) 49 - .into_response(); 50 - } 36 + Err(msg) => return ApiError::InvalidRequest(msg).into_response(), 51 37 }; 52 38 53 39 let _account = match assert_repo_availability(&state.db, &did, false).await { ··· 55 41 Err(e) => return e.into_response(), 56 42 }; 57 43 58 - let mut cids = Vec::new(); 59 - for s in &cid_strings { 60 - match Cid::from_str(s) { 61 - Ok(cid) => cids.push(cid), 62 - Err(_) => return ( 63 - StatusCode::BAD_REQUEST, 64 - Json(json!({"error": "InvalidRequest", "message": format!("Invalid CID: {}", s)})), 65 - ) 66 - .into_response(), 44 + let cids: Vec<Cid> = match cid_strings 45 + .iter() 46 + .map(|s| Cid::from_str(s).map_err(|_| s.clone())) 47 + .collect::<Result<Vec<_>, _>>() 48 + { 49 + Ok(cids) => cids, 50 + Err(invalid) => { 51 + return ApiError::InvalidRequest(format!("Invalid CID: {}", invalid)).into_response() 67 52 } 68 - } 53 + }; 69 54 70 55 if cids.is_empty() { 71 - return ( 72 - StatusCode::BAD_REQUEST, 73 - Json(json!({"error": "InvalidRequest", "message": "No CIDs provided"})), 74 - ) 75 - .into_response(); 56 + return ApiError::InvalidRequest("No CIDs provided".into()).into_response(); 76 57 } 77 58 78 - let blocks_res = state.block_store.get_many(&cids).await; 79 - let blocks = match blocks_res { 59 + let blocks = match state.block_store.get_many(&cids).await { 80 60 Ok(blocks) => blocks, 81 61 Err(e) => { 82 62 error!("Failed to get blocks: {}", e); 83 - return ( 84 - StatusCode::INTERNAL_SERVER_ERROR, 85 - Json(json!({"error": "InternalError", "message": "Failed to get blocks"})), 86 - ) 87 - .into_response(); 63 + return ApiError::InternalError(None).into_response(); 88 64 } 89 65 }; 90 66 91 - let mut missing_cids: Vec<String> = Vec::new(); 92 - for (i, block_opt) in blocks.iter().enumerate() { 93 - if block_opt.is_none() { 94 - missing_cids.push(cids[i].to_string()); 95 - } 96 - } 67 + let missing_cids: Vec<String> = blocks 68 + .iter() 69 + .zip(&cids) 70 + .filter_map(|(block_opt, cid)| block_opt.is_none().then(|| cid.to_string())) 71 + .collect(); 97 72 if !missing_cids.is_empty() { 98 - return ( 99 - StatusCode::BAD_REQUEST, 100 - Json(json!({ 101 - "error": "InvalidRequest", 102 - "message": format!("Could not find blocks: {}", missing_cids.join(", ")) 103 - })), 104 - ) 105 - .into_response(); 73 + return ApiError::InvalidRequest(format!( 74 + "Could not find blocks: {}", 75 + missing_cids.join(", ") 76 + )) 77 + .into_response(); 106 78 } 107 79 108 80 let header = match crate::sync::car::encode_car_header_null_root() { 109 81 Ok(h) => h, 110 82 Err(e) => { 111 83 error!("Failed to encode CAR header: {}", e); 112 - return ( 113 - StatusCode::INTERNAL_SERVER_ERROR, 114 - Json(json!({"error": "InternalError", "message": "Failed to encode CAR"})), 115 - ) 116 - .into_response(); 84 + return ApiError::InternalError(None).into_response(); 117 85 } 118 86 }; 119 87 let mut car_bytes = header; ··· 157 125 Err(e) => return e.into_response(), 158 126 }; 159 127 160 - let head_str = match account.repo_root_cid { 161 - Some(cid) => cid, 162 - None => { 163 - return ( 164 - StatusCode::BAD_REQUEST, 165 - Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 166 - ) 167 - .into_response(); 168 - } 128 + let Some(head_str) = account.repo_root_cid else { 129 + return ApiError::RepoNotFound(Some("Repo not initialized".into())).into_response(); 169 130 }; 170 131 171 - let head_cid = match Cid::from_str(&head_str) { 172 - Ok(c) => c, 173 - Err(_) => { 174 - return ( 175 - StatusCode::INTERNAL_SERVER_ERROR, 176 - Json(json!({"error": "InternalError", "message": "Invalid head CID"})), 177 - ) 178 - .into_response(); 179 - } 132 + let Ok(head_cid) = Cid::from_str(&head_str) else { 133 + return ApiError::InternalError(None).into_response(); 180 134 }; 181 135 182 136 if let Some(since) = &query.since { ··· 186 140 let mut car_bytes = match encode_car_header(&head_cid) { 187 141 Ok(h) => h, 188 142 Err(e) => { 189 - return ( 190 - StatusCode::INTERNAL_SERVER_ERROR, 191 - Json(json!({"error": "InternalError", "message": format!("Failed to encode CAR header: {}", e)})), 192 - ) 193 - .into_response(); 143 + error!("Failed to encode CAR header: {}", e); 144 + return ApiError::InternalError(None).into_response(); 194 145 } 195 146 }; 196 147 let mut stack = vec![head_cid]; ··· 249 200 Ok(e) => e, 250 201 Err(e) => { 251 202 error!("DB error in get_repo_since: {:?}", e); 252 - return ( 253 - StatusCode::INTERNAL_SERVER_ERROR, 254 - Json(json!({"error": "InternalError", "message": "Database error"})), 255 - ) 256 - .into_response(); 203 + return ApiError::InternalError(Some("Database error".into())).into_response(); 257 204 } 258 205 }; 259 206 ··· 279 226 let mut car_bytes = match encode_car_header(head_cid) { 280 227 Ok(h) => h, 281 228 Err(e) => { 282 - return ( 283 - StatusCode::INTERNAL_SERVER_ERROR, 284 - Json(json!({"error": "InternalError", "message": format!("Failed to encode CAR header: {}", e)})), 285 - ) 229 + return ApiError::InternalError(Some(format!("Failed to encode CAR header: {}", e))) 286 230 .into_response(); 287 231 } 288 232 }; ··· 300 244 Ok(b) => b, 301 245 Err(e) => { 302 246 error!("Block store error in get_repo_since: {:?}", e); 303 - return ( 304 - StatusCode::INTERNAL_SERVER_ERROR, 305 - Json(json!({"error": "InternalError", "message": "Failed to get blocks"})), 306 - ) 307 - .into_response(); 247 + return ApiError::InternalError(Some("Failed to get blocks".into())).into_response(); 308 248 } 309 249 }; 310 250 ··· 377 317 let commit_cid_str = match account.repo_root_cid { 378 318 Some(cid) => cid, 379 319 None => { 380 - return ( 381 - StatusCode::BAD_REQUEST, 382 - Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 383 - ) 384 - .into_response(); 320 + return ApiError::RepoNotFound(Some("Repo not initialized".into())).into_response(); 385 321 } 386 322 }; 387 - let commit_cid = match Cid::from_str(&commit_cid_str) { 388 - Ok(c) => c, 389 - Err(_) => { 390 - return ( 391 - StatusCode::INTERNAL_SERVER_ERROR, 392 - Json(json!({"error": "InternalError", "message": "Invalid commit CID"})), 393 - ) 394 - .into_response(); 395 - } 323 + let Ok(commit_cid) = Cid::from_str(&commit_cid_str) else { 324 + return ApiError::InternalError(Some("Invalid commit CID".into())).into_response(); 396 325 }; 397 326 let commit_bytes = match state.block_store.get(&commit_cid).await { 398 327 Ok(Some(b)) => b, 399 328 _ => { 400 - return ( 401 - StatusCode::INTERNAL_SERVER_ERROR, 402 - Json(json!({"error": "InternalError", "message": "Commit block not found"})), 403 - ) 404 - .into_response(); 329 + return ApiError::InternalError(Some("Commit block not found".into())).into_response(); 405 330 } 406 331 }; 407 - let commit = match Commit::from_cbor(&commit_bytes) { 408 - Ok(c) => c, 409 - Err(_) => { 410 - return ( 411 - StatusCode::INTERNAL_SERVER_ERROR, 412 - Json(json!({"error": "InternalError", "message": "Failed to parse commit"})), 413 - ) 414 - .into_response(); 415 - } 332 + let Ok(commit) = Commit::from_cbor(&commit_bytes) else { 333 + return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(); 416 334 }; 417 335 let mst = Mst::load(Arc::new(state.block_store.clone()), commit.data, None); 418 336 let key = format!("{}/{}", query.collection, query.rkey); 419 337 let record_cid = match mst.get(&key).await { 420 338 Ok(Some(cid)) => cid, 421 339 Ok(None) => { 422 - return ( 423 - StatusCode::NOT_FOUND, 424 - Json(json!({"error": "RecordNotFound", "message": "Record not found"})), 425 - ) 426 - .into_response(); 340 + return ApiError::RecordNotFound.into_response(); 427 341 } 428 342 Err(_) => { 429 - return ( 430 - StatusCode::INTERNAL_SERVER_ERROR, 431 - Json(json!({"error": "InternalError", "message": "Failed to lookup record"})), 432 - ) 433 - .into_response(); 343 + return ApiError::InternalError(Some("Failed to lookup record".into())).into_response(); 434 344 } 435 345 }; 436 346 let record_block = match state.block_store.get(&record_cid).await { 437 347 Ok(Some(b)) => b, 438 348 _ => { 439 - return ( 440 - StatusCode::NOT_FOUND, 441 - Json(json!({"error": "RecordNotFound", "message": "Record block not found"})), 442 - ) 443 - .into_response(); 349 + return ApiError::RecordNotFound.into_response(); 444 350 } 445 351 }; 446 352 let mut proof_blocks: BTreeMap<Cid, bytes::Bytes> = BTreeMap::new(); 447 353 if mst.blocks_for_path(&key, &mut proof_blocks).await.is_err() { 448 - return ( 449 - StatusCode::INTERNAL_SERVER_ERROR, 450 - Json(json!({"error": "InternalError", "message": "Failed to build proof path"})), 451 - ) 452 - .into_response(); 354 + return ApiError::InternalError(Some("Failed to build proof path".into())).into_response(); 453 355 } 454 356 let header = match encode_car_header(&commit_cid) { 455 357 Ok(h) => h, 456 358 Err(e) => { 457 359 error!("Failed to encode CAR header: {}", e); 458 - return ( 459 - StatusCode::INTERNAL_SERVER_ERROR, 460 - Json(json!({"error": "InternalError"})), 461 - ) 462 - .into_response(); 360 + return ApiError::InternalError(None).into_response(); 463 361 } 464 362 }; 465 363 let mut car_bytes = header;
+73 -42
src/sync/util.rs
··· 1 + use crate::api::error::ApiError; 1 2 use crate::state::AppState; 2 3 use crate::sync::firehose::SequencedEvent; 3 4 use crate::sync::frame::{ 4 5 AccountFrame, CommitFrame, ErrorFrameBody, ErrorFrameHeader, FrameHeader, IdentityFrame, 5 6 InfoFrame, SyncFrame, 6 7 }; 7 - use axum::Json; 8 - use axum::http::StatusCode; 9 8 use axum::response::{IntoResponse, Response}; 10 9 use bytes::Bytes; 11 10 use cid::Cid; ··· 13 12 use jacquard_repo::commit::Commit; 14 13 use jacquard_repo::storage::BlockStore; 15 14 use serde::Serialize; 16 - use serde_json::json; 17 15 use sqlx::PgPool; 18 16 use std::collections::{BTreeMap, HashMap}; 19 17 use std::io::Cursor; 20 18 use std::str::FromStr; 21 19 use tokio::io::AsyncWriteExt; 22 20 23 - #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 21 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] 24 22 #[serde(rename_all = "lowercase")] 25 23 pub enum AccountStatus { 26 24 Active, ··· 33 31 impl AccountStatus { 34 32 pub fn as_str(&self) -> Option<&'static str> { 35 33 match self { 36 - AccountStatus::Active => None, 37 - AccountStatus::Takendown => Some("takendown"), 38 - AccountStatus::Suspended => Some("suspended"), 39 - AccountStatus::Deactivated => Some("deactivated"), 40 - AccountStatus::Deleted => Some("deleted"), 34 + Self::Active => None, 35 + Self::Takendown => Some("takendown"), 36 + Self::Suspended => Some("suspended"), 37 + Self::Deactivated => Some("deactivated"), 38 + Self::Deleted => Some("deleted"), 41 39 } 42 40 } 43 41 44 42 pub fn is_active(&self) -> bool { 45 - matches!(self, AccountStatus::Active) 43 + matches!(self, Self::Active) 44 + } 45 + 46 + pub fn is_takendown(&self) -> bool { 47 + matches!(self, Self::Takendown) 48 + } 49 + 50 + pub fn is_suspended(&self) -> bool { 51 + matches!(self, Self::Suspended) 52 + } 53 + 54 + pub fn is_deactivated(&self) -> bool { 55 + matches!(self, Self::Deactivated) 56 + } 57 + 58 + pub fn is_deleted(&self) -> bool { 59 + matches!(self, Self::Deleted) 60 + } 61 + 62 + pub fn allows_read(&self) -> bool { 63 + matches!(self, Self::Active | Self::Deactivated) 64 + } 65 + 66 + pub fn allows_write(&self) -> bool { 67 + matches!(self, Self::Active) 68 + } 69 + 70 + pub fn from_db_fields(takedown_ref: Option<&str>, deactivated_at: Option<chrono::DateTime<chrono::Utc>>) -> Self { 71 + if takedown_ref.is_some() { 72 + Self::Takendown 73 + } else if deactivated_at.is_some() { 74 + Self::Deactivated 75 + } else { 76 + Self::Active 77 + } 78 + } 79 + } 80 + 81 + impl From<crate::types::AccountState> for AccountStatus { 82 + fn from(state: crate::types::AccountState) -> Self { 83 + match state { 84 + crate::types::AccountState::Active => AccountStatus::Active, 85 + crate::types::AccountState::Deactivated { .. } => AccountStatus::Deactivated, 86 + crate::types::AccountState::TakenDown { .. } => AccountStatus::Takendown, 87 + crate::types::AccountState::Migrated { .. } => AccountStatus::Deactivated, 88 + } 89 + } 90 + } 91 + 92 + impl From<&crate::types::AccountState> for AccountStatus { 93 + fn from(state: &crate::types::AccountState) -> Self { 94 + match state { 95 + crate::types::AccountState::Active => AccountStatus::Active, 96 + crate::types::AccountState::Deactivated { .. } => AccountStatus::Deactivated, 97 + crate::types::AccountState::TakenDown { .. } => AccountStatus::Takendown, 98 + crate::types::AccountState::Migrated { .. } => AccountStatus::Deactivated, 99 + } 46 100 } 47 101 } 48 102 ··· 63 117 impl IntoResponse for RepoAvailabilityError { 64 118 fn into_response(self) -> Response { 65 119 match self { 66 - RepoAvailabilityError::NotFound(did) => ( 67 - StatusCode::BAD_REQUEST, 68 - Json(json!({ 69 - "error": "RepoNotFound", 70 - "message": format!("Could not find repo for DID: {}", did) 71 - })), 72 - ) 73 - .into_response(), 74 - RepoAvailabilityError::Takendown(did) => ( 75 - StatusCode::BAD_REQUEST, 76 - Json(json!({ 77 - "error": "RepoTakendown", 78 - "message": format!("Repo has been takendown: {}", did) 79 - })), 80 - ) 81 - .into_response(), 82 - RepoAvailabilityError::Deactivated(did) => ( 83 - StatusCode::BAD_REQUEST, 84 - Json(json!({ 85 - "error": "RepoDeactivated", 86 - "message": format!("Repo has been deactivated: {}", did) 87 - })), 88 - ) 89 - .into_response(), 90 - RepoAvailabilityError::Internal(msg) => ( 91 - StatusCode::INTERNAL_SERVER_ERROR, 92 - Json(json!({ 93 - "error": "InternalError", 94 - "message": msg 95 - })), 96 - ) 97 - .into_response(), 120 + RepoAvailabilityError::NotFound(did) => { 121 + ApiError::RepoNotFound(Some(format!("Could not find repo for DID: {}", did))) 122 + .into_response() 123 + } 124 + RepoAvailabilityError::Takendown(_) => ApiError::RepoTakendown.into_response(), 125 + RepoAvailabilityError::Deactivated(_) => ApiError::RepoDeactivated.into_response(), 126 + RepoAvailabilityError::Internal(msg) => { 127 + ApiError::InternalError(Some(msg)).into_response() 128 + } 98 129 } 99 130 } 100 131 }
+1738
src/types.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use std::borrow::Cow; 3 + use std::fmt; 4 + use std::ops::Deref; 5 + use std::str::FromStr; 6 + 7 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, sqlx::Type)] 8 + #[serde(transparent)] 9 + #[sqlx(transparent)] 10 + pub struct Did(String); 11 + 12 + impl<'de> Deserialize<'de> for Did { 13 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 14 + where 15 + D: serde::Deserializer<'de>, 16 + { 17 + let s = String::deserialize(deserializer)?; 18 + Did::new(&s).map_err(|e| serde::de::Error::custom(e.to_string())) 19 + } 20 + } 21 + 22 + impl From<Did> for String { 23 + fn from(did: Did) -> Self { 24 + did.0 25 + } 26 + } 27 + 28 + impl From<String> for Did { 29 + fn from(s: String) -> Self { 30 + Did(s) 31 + } 32 + } 33 + 34 + impl<'a> From<&'a Did> for Cow<'a, str> { 35 + fn from(did: &'a Did) -> Self { 36 + Cow::Borrowed(&did.0) 37 + } 38 + } 39 + 40 + impl Did { 41 + pub fn new(s: impl Into<String>) -> Result<Self, DidError> { 42 + let s = s.into(); 43 + jacquard::types::string::Did::new(&s).map_err(|_| DidError::Invalid(s.clone()))?; 44 + Ok(Self(s)) 45 + } 46 + 47 + pub fn new_unchecked(s: impl Into<String>) -> Self { 48 + Self(s.into()) 49 + } 50 + 51 + pub fn as_str(&self) -> &str { 52 + &self.0 53 + } 54 + 55 + pub fn into_inner(self) -> String { 56 + self.0 57 + } 58 + 59 + pub fn is_plc(&self) -> bool { 60 + self.0.starts_with("did:plc:") 61 + } 62 + 63 + pub fn is_web(&self) -> bool { 64 + self.0.starts_with("did:web:") 65 + } 66 + } 67 + 68 + impl AsRef<str> for Did { 69 + fn as_ref(&self) -> &str { 70 + &self.0 71 + } 72 + } 73 + 74 + impl PartialEq<str> for Did { 75 + fn eq(&self, other: &str) -> bool { 76 + self.0 == other 77 + } 78 + } 79 + 80 + impl PartialEq<&str> for Did { 81 + fn eq(&self, other: &&str) -> bool { 82 + self.0 == *other 83 + } 84 + } 85 + 86 + impl PartialEq<String> for Did { 87 + fn eq(&self, other: &String) -> bool { 88 + self.0 == *other 89 + } 90 + } 91 + 92 + impl PartialEq<Did> for String { 93 + fn eq(&self, other: &Did) -> bool { 94 + *self == other.0 95 + } 96 + } 97 + 98 + impl PartialEq<Did> for &str { 99 + fn eq(&self, other: &Did) -> bool { 100 + *self == other.0 101 + } 102 + } 103 + 104 + impl Deref for Did { 105 + type Target = str; 106 + 107 + fn deref(&self) -> &Self::Target { 108 + &self.0 109 + } 110 + } 111 + 112 + impl fmt::Display for Did { 113 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 + write!(f, "{}", self.0) 115 + } 116 + } 117 + 118 + impl FromStr for Did { 119 + type Err = DidError; 120 + 121 + fn from_str(s: &str) -> Result<Self, Self::Err> { 122 + Self::new(s) 123 + } 124 + } 125 + 126 + #[derive(Debug, Clone)] 127 + pub enum DidError { 128 + Invalid(String), 129 + } 130 + 131 + impl fmt::Display for DidError { 132 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 133 + match self { 134 + Self::Invalid(s) => write!(f, "invalid DID: {}", s), 135 + } 136 + } 137 + } 138 + 139 + impl std::error::Error for DidError {} 140 + 141 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 142 + #[serde(transparent)] 143 + #[sqlx(transparent)] 144 + pub struct Handle(String); 145 + 146 + impl From<Handle> for String { 147 + fn from(handle: Handle) -> Self { 148 + handle.0 149 + } 150 + } 151 + 152 + impl From<String> for Handle { 153 + fn from(s: String) -> Self { 154 + Handle(s) 155 + } 156 + } 157 + 158 + impl<'a> From<&'a Handle> for Cow<'a, str> { 159 + fn from(handle: &'a Handle) -> Self { 160 + Cow::Borrowed(&handle.0) 161 + } 162 + } 163 + 164 + impl Handle { 165 + pub fn new(s: impl Into<String>) -> Result<Self, HandleError> { 166 + let s = s.into(); 167 + jacquard::types::string::Handle::new(&s).map_err(|_| HandleError::Invalid(s.clone()))?; 168 + Ok(Self(s)) 169 + } 170 + 171 + pub fn new_unchecked(s: impl Into<String>) -> Self { 172 + Self(s.into()) 173 + } 174 + 175 + pub fn as_str(&self) -> &str { 176 + &self.0 177 + } 178 + 179 + pub fn into_inner(self) -> String { 180 + self.0 181 + } 182 + } 183 + 184 + impl AsRef<str> for Handle { 185 + fn as_ref(&self) -> &str { 186 + &self.0 187 + } 188 + } 189 + 190 + impl Deref for Handle { 191 + type Target = str; 192 + 193 + fn deref(&self) -> &Self::Target { 194 + &self.0 195 + } 196 + } 197 + 198 + impl PartialEq<str> for Handle { 199 + fn eq(&self, other: &str) -> bool { 200 + self.0 == other 201 + } 202 + } 203 + 204 + impl PartialEq<&str> for Handle { 205 + fn eq(&self, other: &&str) -> bool { 206 + self.0 == *other 207 + } 208 + } 209 + 210 + impl PartialEq<String> for Handle { 211 + fn eq(&self, other: &String) -> bool { 212 + self.0 == *other 213 + } 214 + } 215 + 216 + impl PartialEq<Handle> for String { 217 + fn eq(&self, other: &Handle) -> bool { 218 + *self == other.0 219 + } 220 + } 221 + 222 + impl PartialEq<Handle> for &str { 223 + fn eq(&self, other: &Handle) -> bool { 224 + *self == other.0 225 + } 226 + } 227 + 228 + impl fmt::Display for Handle { 229 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 230 + write!(f, "{}", self.0) 231 + } 232 + } 233 + 234 + impl FromStr for Handle { 235 + type Err = HandleError; 236 + 237 + fn from_str(s: &str) -> Result<Self, Self::Err> { 238 + Self::new(s) 239 + } 240 + } 241 + 242 + #[derive(Debug, Clone)] 243 + pub enum HandleError { 244 + Invalid(String), 245 + } 246 + 247 + impl fmt::Display for HandleError { 248 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 249 + match self { 250 + Self::Invalid(s) => write!(f, "invalid handle: {}", s), 251 + } 252 + } 253 + } 254 + 255 + impl std::error::Error for HandleError {} 256 + 257 + #[derive(Debug, Clone, PartialEq, Eq, Hash)] 258 + pub enum AtIdentifier { 259 + Did(Did), 260 + Handle(Handle), 261 + } 262 + 263 + impl AtIdentifier { 264 + pub fn new(s: impl AsRef<str>) -> Result<Self, AtIdentifierError> { 265 + let s = s.as_ref(); 266 + if s.starts_with("did:") { 267 + Did::new(s) 268 + .map(AtIdentifier::Did) 269 + .map_err(|_| AtIdentifierError::Invalid(s.to_string())) 270 + } else { 271 + Handle::new(s) 272 + .map(AtIdentifier::Handle) 273 + .map_err(|_| AtIdentifierError::Invalid(s.to_string())) 274 + } 275 + } 276 + 277 + pub fn as_str(&self) -> &str { 278 + match self { 279 + AtIdentifier::Did(d) => d.as_str(), 280 + AtIdentifier::Handle(h) => h.as_str(), 281 + } 282 + } 283 + 284 + pub fn into_inner(self) -> String { 285 + match self { 286 + AtIdentifier::Did(d) => d.into_inner(), 287 + AtIdentifier::Handle(h) => h.into_inner(), 288 + } 289 + } 290 + 291 + pub fn is_did(&self) -> bool { 292 + matches!(self, AtIdentifier::Did(_)) 293 + } 294 + 295 + pub fn is_handle(&self) -> bool { 296 + matches!(self, AtIdentifier::Handle(_)) 297 + } 298 + 299 + pub fn as_did(&self) -> Option<&Did> { 300 + match self { 301 + AtIdentifier::Did(d) => Some(d), 302 + AtIdentifier::Handle(_) => None, 303 + } 304 + } 305 + 306 + pub fn as_handle(&self) -> Option<&Handle> { 307 + match self { 308 + AtIdentifier::Handle(h) => Some(h), 309 + AtIdentifier::Did(_) => None, 310 + } 311 + } 312 + } 313 + 314 + impl From<Did> for AtIdentifier { 315 + fn from(did: Did) -> Self { 316 + AtIdentifier::Did(did) 317 + } 318 + } 319 + 320 + impl From<Handle> for AtIdentifier { 321 + fn from(handle: Handle) -> Self { 322 + AtIdentifier::Handle(handle) 323 + } 324 + } 325 + 326 + impl AsRef<str> for AtIdentifier { 327 + fn as_ref(&self) -> &str { 328 + self.as_str() 329 + } 330 + } 331 + 332 + impl Deref for AtIdentifier { 333 + type Target = str; 334 + 335 + fn deref(&self) -> &Self::Target { 336 + self.as_str() 337 + } 338 + } 339 + 340 + impl fmt::Display for AtIdentifier { 341 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 342 + write!(f, "{}", self.as_str()) 343 + } 344 + } 345 + 346 + impl FromStr for AtIdentifier { 347 + type Err = AtIdentifierError; 348 + 349 + fn from_str(s: &str) -> Result<Self, Self::Err> { 350 + Self::new(s) 351 + } 352 + } 353 + 354 + impl Serialize for AtIdentifier { 355 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 356 + where 357 + S: serde::Serializer, 358 + { 359 + serializer.serialize_str(self.as_str()) 360 + } 361 + } 362 + 363 + impl<'de> Deserialize<'de> for AtIdentifier { 364 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 365 + where 366 + D: serde::Deserializer<'de>, 367 + { 368 + let s = String::deserialize(deserializer)?; 369 + AtIdentifier::new(&s).map_err(serde::de::Error::custom) 370 + } 371 + } 372 + 373 + #[derive(Debug, Clone)] 374 + pub enum AtIdentifierError { 375 + Invalid(String), 376 + } 377 + 378 + impl fmt::Display for AtIdentifierError { 379 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 380 + match self { 381 + Self::Invalid(s) => write!(f, "invalid AT identifier: {}", s), 382 + } 383 + } 384 + } 385 + 386 + impl std::error::Error for AtIdentifierError {} 387 + 388 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 389 + #[serde(transparent)] 390 + #[sqlx(type_name = "rkey")] 391 + pub struct Rkey(String); 392 + 393 + impl From<Rkey> for String { 394 + fn from(rkey: Rkey) -> Self { 395 + rkey.0 396 + } 397 + } 398 + 399 + impl From<String> for Rkey { 400 + fn from(s: String) -> Self { 401 + Rkey(s) 402 + } 403 + } 404 + 405 + impl<'a> From<&'a Rkey> for Cow<'a, str> { 406 + fn from(rkey: &'a Rkey) -> Self { 407 + Cow::Borrowed(&rkey.0) 408 + } 409 + } 410 + 411 + impl Rkey { 412 + pub fn new(s: impl Into<String>) -> Result<Self, RkeyError> { 413 + let s = s.into(); 414 + jacquard::types::string::Rkey::new(&s).map_err(|_| RkeyError::Invalid(s.clone()))?; 415 + Ok(Self(s)) 416 + } 417 + 418 + pub fn new_unchecked(s: impl Into<String>) -> Self { 419 + Self(s.into()) 420 + } 421 + 422 + pub fn generate() -> Self { 423 + use jacquard::types::integer::LimitedU32; 424 + Self(jacquard::types::string::Tid::now(LimitedU32::MIN).to_string()) 425 + } 426 + 427 + pub fn as_str(&self) -> &str { 428 + &self.0 429 + } 430 + 431 + pub fn into_inner(self) -> String { 432 + self.0 433 + } 434 + 435 + pub fn is_tid(&self) -> bool { 436 + jacquard::types::string::Tid::from_str(&self.0).is_ok() 437 + } 438 + } 439 + 440 + impl AsRef<str> for Rkey { 441 + fn as_ref(&self) -> &str { 442 + &self.0 443 + } 444 + } 445 + 446 + impl Deref for Rkey { 447 + type Target = str; 448 + 449 + fn deref(&self) -> &Self::Target { 450 + &self.0 451 + } 452 + } 453 + 454 + impl PartialEq<str> for Rkey { 455 + fn eq(&self, other: &str) -> bool { 456 + self.0 == other 457 + } 458 + } 459 + 460 + impl PartialEq<&str> for Rkey { 461 + fn eq(&self, other: &&str) -> bool { 462 + self.0 == *other 463 + } 464 + } 465 + 466 + impl PartialEq<String> for Rkey { 467 + fn eq(&self, other: &String) -> bool { 468 + self.0 == *other 469 + } 470 + } 471 + 472 + impl PartialEq<Rkey> for String { 473 + fn eq(&self, other: &Rkey) -> bool { 474 + *self == other.0 475 + } 476 + } 477 + 478 + impl PartialEq<Rkey> for &str { 479 + fn eq(&self, other: &Rkey) -> bool { 480 + *self == other.0 481 + } 482 + } 483 + 484 + impl fmt::Display for Rkey { 485 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 486 + write!(f, "{}", self.0) 487 + } 488 + } 489 + 490 + impl FromStr for Rkey { 491 + type Err = RkeyError; 492 + 493 + fn from_str(s: &str) -> Result<Self, Self::Err> { 494 + Self::new(s) 495 + } 496 + } 497 + 498 + #[derive(Debug, Clone)] 499 + pub enum RkeyError { 500 + Invalid(String), 501 + } 502 + 503 + impl fmt::Display for RkeyError { 504 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 505 + match self { 506 + Self::Invalid(s) => write!(f, "invalid rkey: {}", s), 507 + } 508 + } 509 + } 510 + 511 + impl std::error::Error for RkeyError {} 512 + 513 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 514 + #[serde(transparent)] 515 + #[sqlx(type_name = "nsid")] 516 + pub struct Nsid(String); 517 + 518 + impl From<Nsid> for String { 519 + fn from(nsid: Nsid) -> Self { 520 + nsid.0 521 + } 522 + } 523 + 524 + impl From<String> for Nsid { 525 + fn from(s: String) -> Self { 526 + Nsid(s) 527 + } 528 + } 529 + 530 + impl<'a> From<&'a Nsid> for Cow<'a, str> { 531 + fn from(nsid: &'a Nsid) -> Self { 532 + Cow::Borrowed(&nsid.0) 533 + } 534 + } 535 + 536 + impl Nsid { 537 + pub fn new(s: impl Into<String>) -> Result<Self, NsidError> { 538 + let s = s.into(); 539 + jacquard::types::string::Nsid::new(&s).map_err(|_| NsidError::Invalid(s.clone()))?; 540 + Ok(Self(s)) 541 + } 542 + 543 + pub fn new_unchecked(s: impl Into<String>) -> Self { 544 + Self(s.into()) 545 + } 546 + 547 + pub fn as_str(&self) -> &str { 548 + &self.0 549 + } 550 + 551 + pub fn into_inner(self) -> String { 552 + self.0 553 + } 554 + 555 + pub fn authority(&self) -> Option<&str> { 556 + let parts: Vec<&str> = self.0.rsplitn(2, '.').collect(); 557 + if parts.len() == 2 { 558 + Some(parts[1]) 559 + } else { 560 + None 561 + } 562 + } 563 + 564 + pub fn name(&self) -> Option<&str> { 565 + self.0.rsplit('.').next() 566 + } 567 + } 568 + 569 + impl AsRef<str> for Nsid { 570 + fn as_ref(&self) -> &str { 571 + &self.0 572 + } 573 + } 574 + 575 + impl Deref for Nsid { 576 + type Target = str; 577 + 578 + fn deref(&self) -> &Self::Target { 579 + &self.0 580 + } 581 + } 582 + 583 + impl PartialEq<str> for Nsid { 584 + fn eq(&self, other: &str) -> bool { 585 + self.0 == other 586 + } 587 + } 588 + 589 + impl PartialEq<&str> for Nsid { 590 + fn eq(&self, other: &&str) -> bool { 591 + self.0 == *other 592 + } 593 + } 594 + 595 + impl PartialEq<String> for Nsid { 596 + fn eq(&self, other: &String) -> bool { 597 + self.0 == *other 598 + } 599 + } 600 + 601 + impl PartialEq<Nsid> for String { 602 + fn eq(&self, other: &Nsid) -> bool { 603 + *self == other.0 604 + } 605 + } 606 + 607 + impl PartialEq<Nsid> for &str { 608 + fn eq(&self, other: &Nsid) -> bool { 609 + *self == other.0 610 + } 611 + } 612 + 613 + impl fmt::Display for Nsid { 614 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 615 + write!(f, "{}", self.0) 616 + } 617 + } 618 + 619 + impl FromStr for Nsid { 620 + type Err = NsidError; 621 + 622 + fn from_str(s: &str) -> Result<Self, Self::Err> { 623 + Self::new(s) 624 + } 625 + } 626 + 627 + #[derive(Debug, Clone)] 628 + pub enum NsidError { 629 + Invalid(String), 630 + } 631 + 632 + impl fmt::Display for NsidError { 633 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 634 + match self { 635 + Self::Invalid(s) => write!(f, "invalid NSID: {}", s), 636 + } 637 + } 638 + } 639 + 640 + impl std::error::Error for NsidError {} 641 + 642 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 643 + #[serde(transparent)] 644 + #[sqlx(type_name = "at_uri")] 645 + pub struct AtUri(String); 646 + 647 + impl From<AtUri> for String { 648 + fn from(uri: AtUri) -> Self { 649 + uri.0 650 + } 651 + } 652 + 653 + impl From<String> for AtUri { 654 + fn from(s: String) -> Self { 655 + AtUri(s) 656 + } 657 + } 658 + 659 + impl<'a> From<&'a AtUri> for Cow<'a, str> { 660 + fn from(uri: &'a AtUri) -> Self { 661 + Cow::Borrowed(&uri.0) 662 + } 663 + } 664 + 665 + impl AtUri { 666 + pub fn new(s: impl Into<String>) -> Result<Self, AtUriError> { 667 + let s = s.into(); 668 + jacquard::types::string::AtUri::new(&s).map_err(|_| AtUriError::Invalid(s.clone()))?; 669 + Ok(Self(s)) 670 + } 671 + 672 + pub fn new_unchecked(s: impl Into<String>) -> Self { 673 + Self(s.into()) 674 + } 675 + 676 + pub fn from_parts(did: &str, collection: &str, rkey: &str) -> Self { 677 + Self(format!("at://{}/{}/{}", did, collection, rkey)) 678 + } 679 + 680 + pub fn as_str(&self) -> &str { 681 + &self.0 682 + } 683 + 684 + pub fn into_inner(self) -> String { 685 + self.0 686 + } 687 + } 688 + 689 + impl AsRef<str> for AtUri { 690 + fn as_ref(&self) -> &str { 691 + &self.0 692 + } 693 + } 694 + 695 + impl Deref for AtUri { 696 + type Target = str; 697 + 698 + fn deref(&self) -> &Self::Target { 699 + &self.0 700 + } 701 + } 702 + 703 + impl PartialEq<str> for AtUri { 704 + fn eq(&self, other: &str) -> bool { 705 + self.0 == other 706 + } 707 + } 708 + 709 + impl PartialEq<&str> for AtUri { 710 + fn eq(&self, other: &&str) -> bool { 711 + self.0 == *other 712 + } 713 + } 714 + 715 + impl PartialEq<String> for AtUri { 716 + fn eq(&self, other: &String) -> bool { 717 + self.0 == *other 718 + } 719 + } 720 + 721 + impl PartialEq<AtUri> for String { 722 + fn eq(&self, other: &AtUri) -> bool { 723 + *self == other.0 724 + } 725 + } 726 + 727 + impl PartialEq<AtUri> for &str { 728 + fn eq(&self, other: &AtUri) -> bool { 729 + *self == other.0 730 + } 731 + } 732 + 733 + impl fmt::Display for AtUri { 734 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 735 + write!(f, "{}", self.0) 736 + } 737 + } 738 + 739 + impl FromStr for AtUri { 740 + type Err = AtUriError; 741 + 742 + fn from_str(s: &str) -> Result<Self, Self::Err> { 743 + Self::new(s) 744 + } 745 + } 746 + 747 + #[derive(Debug, Clone)] 748 + pub enum AtUriError { 749 + Invalid(String), 750 + } 751 + 752 + impl fmt::Display for AtUriError { 753 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 754 + match self { 755 + Self::Invalid(s) => write!(f, "invalid AT URI: {}", s), 756 + } 757 + } 758 + } 759 + 760 + impl std::error::Error for AtUriError {} 761 + 762 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 763 + #[serde(transparent)] 764 + #[sqlx(transparent)] 765 + pub struct Tid(String); 766 + 767 + impl From<Tid> for String { 768 + fn from(tid: Tid) -> Self { 769 + tid.0 770 + } 771 + } 772 + 773 + impl From<String> for Tid { 774 + fn from(s: String) -> Self { 775 + Tid(s) 776 + } 777 + } 778 + 779 + impl<'a> From<&'a Tid> for Cow<'a, str> { 780 + fn from(tid: &'a Tid) -> Self { 781 + Cow::Borrowed(&tid.0) 782 + } 783 + } 784 + 785 + impl Tid { 786 + pub fn new(s: impl Into<String>) -> Result<Self, TidError> { 787 + let s = s.into(); 788 + jacquard::types::string::Tid::from_str(&s).map_err(|_| TidError::Invalid(s.clone()))?; 789 + Ok(Self(s)) 790 + } 791 + 792 + pub fn new_unchecked(s: impl Into<String>) -> Self { 793 + Self(s.into()) 794 + } 795 + 796 + pub fn now() -> Self { 797 + use jacquard::types::integer::LimitedU32; 798 + Self(jacquard::types::string::Tid::now(LimitedU32::MIN).to_string()) 799 + } 800 + 801 + pub fn as_str(&self) -> &str { 802 + &self.0 803 + } 804 + 805 + pub fn into_inner(self) -> String { 806 + self.0 807 + } 808 + } 809 + 810 + impl AsRef<str> for Tid { 811 + fn as_ref(&self) -> &str { 812 + &self.0 813 + } 814 + } 815 + 816 + impl Deref for Tid { 817 + type Target = str; 818 + 819 + fn deref(&self) -> &Self::Target { 820 + &self.0 821 + } 822 + } 823 + 824 + impl PartialEq<str> for Tid { 825 + fn eq(&self, other: &str) -> bool { 826 + self.0 == other 827 + } 828 + } 829 + 830 + impl PartialEq<&str> for Tid { 831 + fn eq(&self, other: &&str) -> bool { 832 + self.0 == *other 833 + } 834 + } 835 + 836 + impl PartialEq<String> for Tid { 837 + fn eq(&self, other: &String) -> bool { 838 + self.0 == *other 839 + } 840 + } 841 + 842 + impl PartialEq<Tid> for String { 843 + fn eq(&self, other: &Tid) -> bool { 844 + *self == other.0 845 + } 846 + } 847 + 848 + impl PartialEq<Tid> for &str { 849 + fn eq(&self, other: &Tid) -> bool { 850 + *self == other.0 851 + } 852 + } 853 + 854 + impl fmt::Display for Tid { 855 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 856 + write!(f, "{}", self.0) 857 + } 858 + } 859 + 860 + impl FromStr for Tid { 861 + type Err = TidError; 862 + 863 + fn from_str(s: &str) -> Result<Self, Self::Err> { 864 + Self::new(s) 865 + } 866 + } 867 + 868 + #[derive(Debug, Clone)] 869 + pub enum TidError { 870 + Invalid(String), 871 + } 872 + 873 + impl fmt::Display for TidError { 874 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 875 + match self { 876 + Self::Invalid(s) => write!(f, "invalid TID: {}", s), 877 + } 878 + } 879 + } 880 + 881 + impl std::error::Error for TidError {} 882 + 883 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 884 + #[serde(transparent)] 885 + #[sqlx(transparent)] 886 + pub struct Datetime(String); 887 + 888 + impl From<Datetime> for String { 889 + fn from(dt: Datetime) -> Self { 890 + dt.0 891 + } 892 + } 893 + 894 + impl From<String> for Datetime { 895 + fn from(s: String) -> Self { 896 + Datetime(s) 897 + } 898 + } 899 + 900 + impl<'a> From<&'a Datetime> for Cow<'a, str> { 901 + fn from(dt: &'a Datetime) -> Self { 902 + Cow::Borrowed(&dt.0) 903 + } 904 + } 905 + 906 + impl Datetime { 907 + pub fn new(s: impl Into<String>) -> Result<Self, DatetimeError> { 908 + let s = s.into(); 909 + jacquard::types::string::Datetime::from_str(&s) 910 + .map_err(|_| DatetimeError::Invalid(s.clone()))?; 911 + Ok(Self(s)) 912 + } 913 + 914 + pub fn new_unchecked(s: impl Into<String>) -> Self { 915 + Self(s.into()) 916 + } 917 + 918 + pub fn now() -> Self { 919 + Self(chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()) 920 + } 921 + 922 + pub fn as_str(&self) -> &str { 923 + &self.0 924 + } 925 + 926 + pub fn into_inner(self) -> String { 927 + self.0 928 + } 929 + } 930 + 931 + impl AsRef<str> for Datetime { 932 + fn as_ref(&self) -> &str { 933 + &self.0 934 + } 935 + } 936 + 937 + impl Deref for Datetime { 938 + type Target = str; 939 + 940 + fn deref(&self) -> &Self::Target { 941 + &self.0 942 + } 943 + } 944 + 945 + impl PartialEq<str> for Datetime { 946 + fn eq(&self, other: &str) -> bool { 947 + self.0 == other 948 + } 949 + } 950 + 951 + impl PartialEq<&str> for Datetime { 952 + fn eq(&self, other: &&str) -> bool { 953 + self.0 == *other 954 + } 955 + } 956 + 957 + impl PartialEq<String> for Datetime { 958 + fn eq(&self, other: &String) -> bool { 959 + self.0 == *other 960 + } 961 + } 962 + 963 + impl PartialEq<Datetime> for String { 964 + fn eq(&self, other: &Datetime) -> bool { 965 + *self == other.0 966 + } 967 + } 968 + 969 + impl PartialEq<Datetime> for &str { 970 + fn eq(&self, other: &Datetime) -> bool { 971 + *self == other.0 972 + } 973 + } 974 + 975 + impl fmt::Display for Datetime { 976 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 977 + write!(f, "{}", self.0) 978 + } 979 + } 980 + 981 + impl FromStr for Datetime { 982 + type Err = DatetimeError; 983 + 984 + fn from_str(s: &str) -> Result<Self, Self::Err> { 985 + Self::new(s) 986 + } 987 + } 988 + 989 + #[derive(Debug, Clone)] 990 + pub enum DatetimeError { 991 + Invalid(String), 992 + } 993 + 994 + impl fmt::Display for DatetimeError { 995 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 996 + match self { 997 + Self::Invalid(s) => write!(f, "invalid datetime: {}", s), 998 + } 999 + } 1000 + } 1001 + 1002 + impl std::error::Error for DatetimeError {} 1003 + 1004 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1005 + #[serde(transparent)] 1006 + #[sqlx(transparent)] 1007 + pub struct Language(String); 1008 + 1009 + impl From<Language> for String { 1010 + fn from(lang: Language) -> Self { 1011 + lang.0 1012 + } 1013 + } 1014 + 1015 + impl From<String> for Language { 1016 + fn from(s: String) -> Self { 1017 + Language(s) 1018 + } 1019 + } 1020 + 1021 + impl<'a> From<&'a Language> for Cow<'a, str> { 1022 + fn from(lang: &'a Language) -> Self { 1023 + Cow::Borrowed(&lang.0) 1024 + } 1025 + } 1026 + 1027 + impl Language { 1028 + pub fn new(s: impl Into<String>) -> Result<Self, LanguageError> { 1029 + let s = s.into(); 1030 + jacquard::types::string::Language::from_str(&s) 1031 + .map_err(|_| LanguageError::Invalid(s.clone()))?; 1032 + Ok(Self(s)) 1033 + } 1034 + 1035 + pub fn new_unchecked(s: impl Into<String>) -> Self { 1036 + Self(s.into()) 1037 + } 1038 + 1039 + pub fn as_str(&self) -> &str { 1040 + &self.0 1041 + } 1042 + 1043 + pub fn into_inner(self) -> String { 1044 + self.0 1045 + } 1046 + } 1047 + 1048 + impl AsRef<str> for Language { 1049 + fn as_ref(&self) -> &str { 1050 + &self.0 1051 + } 1052 + } 1053 + 1054 + impl Deref for Language { 1055 + type Target = str; 1056 + 1057 + fn deref(&self) -> &Self::Target { 1058 + &self.0 1059 + } 1060 + } 1061 + 1062 + impl PartialEq<str> for Language { 1063 + fn eq(&self, other: &str) -> bool { 1064 + self.0 == other 1065 + } 1066 + } 1067 + 1068 + impl PartialEq<&str> for Language { 1069 + fn eq(&self, other: &&str) -> bool { 1070 + self.0 == *other 1071 + } 1072 + } 1073 + 1074 + impl PartialEq<String> for Language { 1075 + fn eq(&self, other: &String) -> bool { 1076 + self.0 == *other 1077 + } 1078 + } 1079 + 1080 + impl PartialEq<Language> for String { 1081 + fn eq(&self, other: &Language) -> bool { 1082 + *self == other.0 1083 + } 1084 + } 1085 + 1086 + impl PartialEq<Language> for &str { 1087 + fn eq(&self, other: &Language) -> bool { 1088 + *self == other.0 1089 + } 1090 + } 1091 + 1092 + impl fmt::Display for Language { 1093 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1094 + write!(f, "{}", self.0) 1095 + } 1096 + } 1097 + 1098 + impl FromStr for Language { 1099 + type Err = LanguageError; 1100 + 1101 + fn from_str(s: &str) -> Result<Self, Self::Err> { 1102 + Self::new(s) 1103 + } 1104 + } 1105 + 1106 + #[derive(Debug, Clone)] 1107 + pub enum LanguageError { 1108 + Invalid(String), 1109 + } 1110 + 1111 + impl fmt::Display for LanguageError { 1112 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1113 + match self { 1114 + Self::Invalid(s) => write!(f, "invalid language tag: {}", s), 1115 + } 1116 + } 1117 + } 1118 + 1119 + impl std::error::Error for LanguageError {} 1120 + 1121 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 1122 + #[serde(transparent)] 1123 + #[sqlx(transparent)] 1124 + pub struct CidLink(String); 1125 + 1126 + impl From<CidLink> for String { 1127 + fn from(cid: CidLink) -> Self { 1128 + cid.0 1129 + } 1130 + } 1131 + 1132 + impl From<String> for CidLink { 1133 + fn from(s: String) -> Self { 1134 + CidLink(s) 1135 + } 1136 + } 1137 + 1138 + impl<'a> From<&'a CidLink> for Cow<'a, str> { 1139 + fn from(cid: &'a CidLink) -> Self { 1140 + Cow::Borrowed(&cid.0) 1141 + } 1142 + } 1143 + 1144 + impl CidLink { 1145 + pub fn new(s: impl Into<String>) -> Result<Self, CidLinkError> { 1146 + let s = s.into(); 1147 + cid::Cid::from_str(&s).map_err(|_| CidLinkError::Invalid(s.clone()))?; 1148 + Ok(Self(s)) 1149 + } 1150 + 1151 + pub fn new_unchecked(s: impl Into<String>) -> Self { 1152 + Self(s.into()) 1153 + } 1154 + 1155 + pub fn as_str(&self) -> &str { 1156 + &self.0 1157 + } 1158 + 1159 + pub fn into_inner(self) -> String { 1160 + self.0 1161 + } 1162 + 1163 + pub fn to_cid(&self) -> Result<cid::Cid, cid::Error> { 1164 + cid::Cid::from_str(&self.0) 1165 + } 1166 + } 1167 + 1168 + impl AsRef<str> for CidLink { 1169 + fn as_ref(&self) -> &str { 1170 + &self.0 1171 + } 1172 + } 1173 + 1174 + impl Deref for CidLink { 1175 + type Target = str; 1176 + 1177 + fn deref(&self) -> &Self::Target { 1178 + &self.0 1179 + } 1180 + } 1181 + 1182 + impl PartialEq<str> for CidLink { 1183 + fn eq(&self, other: &str) -> bool { 1184 + self.0 == other 1185 + } 1186 + } 1187 + 1188 + impl PartialEq<&str> for CidLink { 1189 + fn eq(&self, other: &&str) -> bool { 1190 + self.0 == *other 1191 + } 1192 + } 1193 + 1194 + impl PartialEq<String> for CidLink { 1195 + fn eq(&self, other: &String) -> bool { 1196 + self.0 == *other 1197 + } 1198 + } 1199 + 1200 + impl PartialEq<CidLink> for String { 1201 + fn eq(&self, other: &CidLink) -> bool { 1202 + *self == other.0 1203 + } 1204 + } 1205 + 1206 + impl PartialEq<CidLink> for &str { 1207 + fn eq(&self, other: &CidLink) -> bool { 1208 + *self == other.0 1209 + } 1210 + } 1211 + 1212 + impl fmt::Display for CidLink { 1213 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1214 + write!(f, "{}", self.0) 1215 + } 1216 + } 1217 + 1218 + impl FromStr for CidLink { 1219 + type Err = CidLinkError; 1220 + 1221 + fn from_str(s: &str) -> Result<Self, Self::Err> { 1222 + Self::new(s) 1223 + } 1224 + } 1225 + 1226 + #[derive(Debug, Clone)] 1227 + pub enum CidLinkError { 1228 + Invalid(String), 1229 + } 1230 + 1231 + impl fmt::Display for CidLinkError { 1232 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1233 + match self { 1234 + Self::Invalid(s) => write!(f, "invalid CID: {}", s), 1235 + } 1236 + } 1237 + } 1238 + 1239 + impl std::error::Error for CidLinkError {} 1240 + 1241 + #[derive(Debug, Clone, PartialEq, Eq)] 1242 + pub enum AccountState { 1243 + Active, 1244 + Deactivated { 1245 + at: chrono::DateTime<chrono::Utc>, 1246 + }, 1247 + TakenDown { 1248 + reference: String, 1249 + }, 1250 + Migrated { 1251 + at: chrono::DateTime<chrono::Utc>, 1252 + to_pds: String, 1253 + }, 1254 + } 1255 + 1256 + impl AccountState { 1257 + pub fn from_db_fields( 1258 + deactivated_at: Option<chrono::DateTime<chrono::Utc>>, 1259 + takedown_ref: Option<String>, 1260 + migrated_to_pds: Option<String>, 1261 + migrated_at: Option<chrono::DateTime<chrono::Utc>>, 1262 + ) -> Self { 1263 + if let Some(reference) = takedown_ref { 1264 + AccountState::TakenDown { reference } 1265 + } else if let (Some(at), Some(to_pds)) = (deactivated_at, migrated_to_pds) { 1266 + let migrated_at = migrated_at.unwrap_or(at); 1267 + AccountState::Migrated { 1268 + at: migrated_at, 1269 + to_pds, 1270 + } 1271 + } else if let Some(at) = deactivated_at { 1272 + AccountState::Deactivated { at } 1273 + } else { 1274 + AccountState::Active 1275 + } 1276 + } 1277 + 1278 + pub fn is_active(&self) -> bool { 1279 + matches!(self, AccountState::Active) 1280 + } 1281 + 1282 + pub fn is_deactivated(&self) -> bool { 1283 + matches!(self, AccountState::Deactivated { .. }) 1284 + } 1285 + 1286 + pub fn is_takendown(&self) -> bool { 1287 + matches!(self, AccountState::TakenDown { .. }) 1288 + } 1289 + 1290 + pub fn is_migrated(&self) -> bool { 1291 + matches!(self, AccountState::Migrated { .. }) 1292 + } 1293 + 1294 + pub fn can_login(&self) -> bool { 1295 + matches!(self, AccountState::Active) 1296 + } 1297 + 1298 + pub fn can_access_repo(&self) -> bool { 1299 + matches!(self, AccountState::Active | AccountState::Deactivated { .. }) 1300 + } 1301 + 1302 + pub fn status_string(&self) -> &'static str { 1303 + match self { 1304 + AccountState::Active => "active", 1305 + AccountState::Deactivated { .. } => "deactivated", 1306 + AccountState::TakenDown { .. } => "takendown", 1307 + AccountState::Migrated { .. } => "deactivated", 1308 + } 1309 + } 1310 + 1311 + pub fn status_for_session(&self) -> Option<&'static str> { 1312 + match self { 1313 + AccountState::Active => None, 1314 + AccountState::Deactivated { .. } => Some("deactivated"), 1315 + AccountState::TakenDown { .. } => Some("takendown"), 1316 + AccountState::Migrated { .. } => Some("migrated"), 1317 + } 1318 + } 1319 + } 1320 + 1321 + impl fmt::Display for AccountState { 1322 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1323 + match self { 1324 + AccountState::Active => write!(f, "active"), 1325 + AccountState::Deactivated { at } => write!(f, "deactivated ({})", at), 1326 + AccountState::TakenDown { reference } => write!(f, "takendown ({})", reference), 1327 + AccountState::Migrated { to_pds, .. } => write!(f, "migrated to {}", to_pds), 1328 + } 1329 + } 1330 + } 1331 + 1332 + #[derive(Debug, Clone, Deserialize)] 1333 + #[serde(transparent)] 1334 + pub struct PlainPassword(String); 1335 + 1336 + impl PlainPassword { 1337 + pub fn new(s: impl Into<String>) -> Self { 1338 + Self(s.into()) 1339 + } 1340 + 1341 + pub fn as_str(&self) -> &str { 1342 + &self.0 1343 + } 1344 + 1345 + pub fn into_inner(self) -> String { 1346 + self.0 1347 + } 1348 + 1349 + pub fn is_empty(&self) -> bool { 1350 + self.0.is_empty() 1351 + } 1352 + } 1353 + 1354 + impl AsRef<str> for PlainPassword { 1355 + fn as_ref(&self) -> &str { 1356 + &self.0 1357 + } 1358 + } 1359 + 1360 + impl AsRef<[u8]> for PlainPassword { 1361 + fn as_ref(&self) -> &[u8] { 1362 + self.0.as_bytes() 1363 + } 1364 + } 1365 + 1366 + impl Deref for PlainPassword { 1367 + type Target = str; 1368 + 1369 + fn deref(&self) -> &Self::Target { 1370 + &self.0 1371 + } 1372 + } 1373 + 1374 + #[derive(Debug, Clone, Serialize, sqlx::Type)] 1375 + #[serde(transparent)] 1376 + #[sqlx(transparent)] 1377 + pub struct PasswordHash(String); 1378 + 1379 + impl PasswordHash { 1380 + pub fn from_hash(hash: impl Into<String>) -> Self { 1381 + Self(hash.into()) 1382 + } 1383 + 1384 + pub fn as_str(&self) -> &str { 1385 + &self.0 1386 + } 1387 + 1388 + pub fn into_inner(self) -> String { 1389 + self.0 1390 + } 1391 + } 1392 + 1393 + impl AsRef<str> for PasswordHash { 1394 + fn as_ref(&self) -> &str { 1395 + &self.0 1396 + } 1397 + } 1398 + 1399 + impl From<String> for PasswordHash { 1400 + fn from(s: String) -> Self { 1401 + Self(s) 1402 + } 1403 + } 1404 + 1405 + #[derive(Debug, Clone, PartialEq, Eq)] 1406 + pub enum TokenSource { 1407 + Session, 1408 + OAuth { client_id: Option<String> }, 1409 + ServiceAuth { lxm: Option<String>, aud: Option<String> }, 1410 + } 1411 + 1412 + impl TokenSource { 1413 + pub fn is_session(&self) -> bool { 1414 + matches!(self, TokenSource::Session) 1415 + } 1416 + 1417 + pub fn is_oauth(&self) -> bool { 1418 + matches!(self, TokenSource::OAuth { .. }) 1419 + } 1420 + 1421 + pub fn is_service_auth(&self) -> bool { 1422 + matches!(self, TokenSource::ServiceAuth { .. }) 1423 + } 1424 + } 1425 + 1426 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 1427 + #[serde(transparent)] 1428 + pub struct JwkThumbprint(String); 1429 + 1430 + impl JwkThumbprint { 1431 + pub fn new(s: impl Into<String>) -> Self { 1432 + Self(s.into()) 1433 + } 1434 + 1435 + pub fn as_str(&self) -> &str { 1436 + &self.0 1437 + } 1438 + 1439 + pub fn into_inner(self) -> String { 1440 + self.0 1441 + } 1442 + } 1443 + 1444 + impl AsRef<str> for JwkThumbprint { 1445 + fn as_ref(&self) -> &str { 1446 + &self.0 1447 + } 1448 + } 1449 + 1450 + impl Deref for JwkThumbprint { 1451 + type Target = str; 1452 + 1453 + fn deref(&self) -> &Self::Target { 1454 + &self.0 1455 + } 1456 + } 1457 + 1458 + impl From<String> for JwkThumbprint { 1459 + fn from(s: String) -> Self { 1460 + Self(s) 1461 + } 1462 + } 1463 + 1464 + impl PartialEq<str> for JwkThumbprint { 1465 + fn eq(&self, other: &str) -> bool { 1466 + self.0 == other 1467 + } 1468 + } 1469 + 1470 + impl PartialEq<String> for JwkThumbprint { 1471 + fn eq(&self, other: &String) -> bool { 1472 + &self.0 == other 1473 + } 1474 + } 1475 + 1476 + impl PartialEq<JwkThumbprint> for String { 1477 + fn eq(&self, other: &JwkThumbprint) -> bool { 1478 + self == &other.0 1479 + } 1480 + } 1481 + 1482 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 1483 + #[serde(transparent)] 1484 + pub struct DPoPProofId(String); 1485 + 1486 + impl DPoPProofId { 1487 + pub fn new(s: impl Into<String>) -> Self { 1488 + Self(s.into()) 1489 + } 1490 + 1491 + pub fn as_str(&self) -> &str { 1492 + &self.0 1493 + } 1494 + 1495 + pub fn into_inner(self) -> String { 1496 + self.0 1497 + } 1498 + } 1499 + 1500 + impl AsRef<str> for DPoPProofId { 1501 + fn as_ref(&self) -> &str { 1502 + &self.0 1503 + } 1504 + } 1505 + 1506 + impl Deref for DPoPProofId { 1507 + type Target = str; 1508 + 1509 + fn deref(&self) -> &Self::Target { 1510 + &self.0 1511 + } 1512 + } 1513 + 1514 + impl From<String> for DPoPProofId { 1515 + fn from(s: String) -> Self { 1516 + Self(s) 1517 + } 1518 + } 1519 + 1520 + #[cfg(test)] 1521 + mod tests { 1522 + use super::*; 1523 + 1524 + #[test] 1525 + fn test_did_validation() { 1526 + assert!(Did::new("did:plc:abc123").is_ok()); 1527 + assert!(Did::new("did:web:example.com").is_ok()); 1528 + assert!(Did::new("not-a-did").is_err()); 1529 + assert!(Did::new("").is_err()); 1530 + } 1531 + 1532 + #[test] 1533 + fn test_did_methods() { 1534 + let plc = Did::new("did:plc:abc123").unwrap(); 1535 + assert!(plc.is_plc()); 1536 + assert!(!plc.is_web()); 1537 + assert_eq!(plc.as_str(), "did:plc:abc123"); 1538 + 1539 + let web = Did::new("did:web:example.com").unwrap(); 1540 + assert!(!web.is_plc()); 1541 + assert!(web.is_web()); 1542 + } 1543 + 1544 + #[test] 1545 + fn test_did_conversions() { 1546 + let did = Did::new("did:plc:test123").unwrap(); 1547 + let s: String = did.clone().into(); 1548 + assert_eq!(s, "did:plc:test123"); 1549 + assert_eq!(format!("{}", did), "did:plc:test123"); 1550 + } 1551 + 1552 + #[test] 1553 + fn test_did_serde() { 1554 + let did = Did::new("did:plc:test123").unwrap(); 1555 + let json = serde_json::to_string(&did).unwrap(); 1556 + assert_eq!(json, "\"did:plc:test123\""); 1557 + 1558 + let parsed: Did = serde_json::from_str(&json).unwrap(); 1559 + assert_eq!(parsed, did); 1560 + } 1561 + 1562 + #[test] 1563 + fn test_handle_validation() { 1564 + assert!(Handle::new("user.bsky.social").is_ok()); 1565 + assert!(Handle::new("test.example.com").is_ok()); 1566 + assert!(Handle::new("invalid handle with spaces").is_err()); 1567 + } 1568 + 1569 + #[test] 1570 + fn test_rkey_validation() { 1571 + assert!(Rkey::new("self").is_ok()); 1572 + assert!(Rkey::new("3jzfcijpj2z2a").is_ok()); 1573 + assert!(Rkey::new("invalid/rkey").is_err()); 1574 + } 1575 + 1576 + #[test] 1577 + fn test_rkey_generate() { 1578 + let rkey = Rkey::generate(); 1579 + assert!(rkey.is_tid()); 1580 + assert!(!rkey.as_str().is_empty()); 1581 + } 1582 + 1583 + #[test] 1584 + fn test_nsid_validation() { 1585 + assert!(Nsid::new("app.bsky.feed.post").is_ok()); 1586 + assert!(Nsid::new("com.atproto.repo.createRecord").is_ok()); 1587 + assert!(Nsid::new("invalid").is_err()); 1588 + } 1589 + 1590 + #[test] 1591 + fn test_nsid_parts() { 1592 + let nsid = Nsid::new("app.bsky.feed.post").unwrap(); 1593 + assert_eq!(nsid.name(), Some("post")); 1594 + } 1595 + 1596 + #[test] 1597 + fn test_at_uri_validation() { 1598 + assert!(AtUri::new("at://did:plc:abc123/app.bsky.feed.post/xyz").is_ok()); 1599 + assert!(AtUri::new("not-an-at-uri").is_err()); 1600 + } 1601 + 1602 + #[test] 1603 + fn test_at_uri_from_parts() { 1604 + let uri = AtUri::from_parts("did:plc:abc123", "app.bsky.feed.post", "xyz"); 1605 + assert_eq!(uri.as_str(), "at://did:plc:abc123/app.bsky.feed.post/xyz"); 1606 + } 1607 + 1608 + #[test] 1609 + fn test_type_safety() { 1610 + fn takes_did(_: &Did) {} 1611 + fn takes_handle(_: &Handle) {} 1612 + 1613 + let did = Did::new("did:plc:test").unwrap(); 1614 + let handle = Handle::new("test.bsky.social").unwrap(); 1615 + 1616 + takes_did(&did); 1617 + takes_handle(&handle); 1618 + } 1619 + 1620 + #[test] 1621 + fn test_tid_validation() { 1622 + let tid = Tid::now(); 1623 + assert!(!tid.as_str().is_empty()); 1624 + assert!(Tid::new(tid.as_str()).is_ok()); 1625 + assert!(Tid::new("invalid").is_err()); 1626 + } 1627 + 1628 + #[test] 1629 + fn test_datetime_validation() { 1630 + assert!(Datetime::new("2024-01-15T12:30:45.123Z").is_ok()); 1631 + assert!(Datetime::new("not-a-date").is_err()); 1632 + let now = Datetime::now(); 1633 + assert!(!now.as_str().is_empty()); 1634 + } 1635 + 1636 + #[test] 1637 + fn test_language_validation() { 1638 + assert!(Language::new("en").is_ok()); 1639 + assert!(Language::new("en-US").is_ok()); 1640 + assert!(Language::new("ja").is_ok()); 1641 + } 1642 + 1643 + #[test] 1644 + fn test_cidlink_validation() { 1645 + assert!(CidLink::new("bafyreib74ckyq525l3y6an5txykwwtb3dgex6ofzakml53di77oxwr5pfe").is_ok()); 1646 + assert!(CidLink::new("not-a-cid").is_err()); 1647 + } 1648 + 1649 + #[test] 1650 + fn test_at_identifier_validation() { 1651 + let did_ident = AtIdentifier::new("did:plc:abc123").unwrap(); 1652 + assert!(did_ident.is_did()); 1653 + assert!(!did_ident.is_handle()); 1654 + assert!(did_ident.as_did().is_some()); 1655 + assert!(did_ident.as_handle().is_none()); 1656 + 1657 + let handle_ident = AtIdentifier::new("user.bsky.social").unwrap(); 1658 + assert!(!handle_ident.is_did()); 1659 + assert!(handle_ident.is_handle()); 1660 + assert!(handle_ident.as_did().is_none()); 1661 + assert!(handle_ident.as_handle().is_some()); 1662 + 1663 + assert!(AtIdentifier::new("invalid identifier").is_err()); 1664 + } 1665 + 1666 + #[test] 1667 + fn test_at_identifier_serde() { 1668 + let ident = AtIdentifier::new("did:plc:test123").unwrap(); 1669 + let json = serde_json::to_string(&ident).unwrap(); 1670 + assert_eq!(json, "\"did:plc:test123\""); 1671 + 1672 + let parsed: AtIdentifier = serde_json::from_str(&json).unwrap(); 1673 + assert_eq!(parsed.as_str(), "did:plc:test123"); 1674 + } 1675 + 1676 + #[test] 1677 + fn test_account_state_active() { 1678 + let state = AccountState::from_db_fields(None, None, None, None); 1679 + assert!(state.is_active()); 1680 + assert!(!state.is_deactivated()); 1681 + assert!(!state.is_takendown()); 1682 + assert!(!state.is_migrated()); 1683 + assert!(state.can_login()); 1684 + assert!(state.can_access_repo()); 1685 + assert_eq!(state.status_string(), "active"); 1686 + } 1687 + 1688 + #[test] 1689 + fn test_account_state_deactivated() { 1690 + let now = chrono::Utc::now(); 1691 + let state = AccountState::from_db_fields(Some(now), None, None, None); 1692 + assert!(!state.is_active()); 1693 + assert!(state.is_deactivated()); 1694 + assert!(!state.is_takendown()); 1695 + assert!(!state.is_migrated()); 1696 + assert!(!state.can_login()); 1697 + assert!(state.can_access_repo()); 1698 + assert_eq!(state.status_string(), "deactivated"); 1699 + } 1700 + 1701 + #[test] 1702 + fn test_account_state_takendown() { 1703 + let state = AccountState::from_db_fields(None, Some("mod-action-123".into()), None, None); 1704 + assert!(!state.is_active()); 1705 + assert!(!state.is_deactivated()); 1706 + assert!(state.is_takendown()); 1707 + assert!(!state.is_migrated()); 1708 + assert!(!state.can_login()); 1709 + assert!(!state.can_access_repo()); 1710 + assert_eq!(state.status_string(), "takendown"); 1711 + } 1712 + 1713 + #[test] 1714 + fn test_account_state_migrated() { 1715 + let now = chrono::Utc::now(); 1716 + let state = 1717 + AccountState::from_db_fields(Some(now), None, Some("https://other.pds".into()), None); 1718 + assert!(!state.is_active()); 1719 + assert!(!state.is_deactivated()); 1720 + assert!(!state.is_takendown()); 1721 + assert!(state.is_migrated()); 1722 + assert!(!state.can_login()); 1723 + assert!(!state.can_access_repo()); 1724 + assert_eq!(state.status_string(), "deactivated"); 1725 + } 1726 + 1727 + #[test] 1728 + fn test_account_state_takedown_priority() { 1729 + let now = chrono::Utc::now(); 1730 + let state = AccountState::from_db_fields( 1731 + Some(now), 1732 + Some("mod-action".into()), 1733 + Some("https://other.pds".into()), 1734 + None, 1735 + ); 1736 + assert!(state.is_takendown()); 1737 + } 1738 + }
+4 -2
src/util.rs
··· 9 9 use std::sync::OnceLock; 10 10 use uuid::Uuid; 11 11 12 + use crate::types::{Did, Handle}; 13 + 12 14 const BASE32_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz234567"; 13 15 const DEFAULT_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024 * 1024; 14 16 ··· 62 64 63 65 pub struct UserInfo { 64 66 pub id: Uuid, 65 - pub did: String, 66 - pub handle: String, 67 + pub did: Did, 68 + pub handle: Handle, 67 69 } 68 70 69 71 pub async fn get_user_by_did(db: &PgPool, did: &str) -> Result<UserInfo, DbLookupError> {
+28 -26
src/validation/mod.rs
··· 28 28 Invalid, 29 29 } 30 30 31 + impl std::fmt::Display for ValidationStatus { 32 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 + match self { 34 + Self::Valid => write!(f, "valid"), 35 + Self::Unknown => write!(f, "unknown"), 36 + Self::Invalid => write!(f, "invalid"), 37 + } 38 + } 39 + } 40 + 31 41 pub struct RecordValidator { 32 42 require_lexicon: bool, 33 43 } ··· 553 563 impl std::error::Error for PasswordValidationError {} 554 564 555 565 pub fn validate_password(password: &str) -> Result<(), PasswordValidationError> { 556 - let mut errors = Vec::new(); 557 - 558 - if password.len() < 8 { 559 - errors.push("Password must be at least 8 characters".to_string()); 560 - } 561 - 562 - if password.len() > 256 { 563 - errors.push("Password must be at most 256 characters".to_string()); 564 - } 565 - 566 - if !password.chars().any(|c| c.is_ascii_lowercase()) { 567 - errors.push("Password must contain at least one lowercase letter".to_string()); 568 - } 569 - 570 - if !password.chars().any(|c| c.is_ascii_uppercase()) { 571 - errors.push("Password must contain at least one uppercase letter".to_string()); 572 - } 573 - 574 - if !password.chars().any(|c| c.is_ascii_digit()) { 575 - errors.push("Password must contain at least one number".to_string()); 576 - } 577 - 578 - if is_common_password(password) { 579 - errors.push("Password is too common, please choose a different one".to_string()); 580 - } 566 + let errors: Vec<&'static str> = [ 567 + (password.len() < 8).then_some("Password must be at least 8 characters"), 568 + (password.len() > 256).then_some("Password must be at most 256 characters"), 569 + (!password.chars().any(|c| c.is_ascii_lowercase())) 570 + .then_some("Password must contain at least one lowercase letter"), 571 + (!password.chars().any(|c| c.is_ascii_uppercase())) 572 + .then_some("Password must contain at least one uppercase letter"), 573 + (!password.chars().any(|c| c.is_ascii_digit())) 574 + .then_some("Password must contain at least one number"), 575 + is_common_password(password) 576 + .then_some("Password is too common, please choose a different one"), 577 + ] 578 + .into_iter() 579 + .flatten() 580 + .collect(); 581 581 582 582 if errors.is_empty() { 583 583 Ok(()) 584 584 } else { 585 - Err(PasswordValidationError { errors }) 585 + Err(PasswordValidationError { 586 + errors: errors.iter().map(|s| (*s).to_string()).collect(), 587 + }) 586 588 } 587 589 } 588 590
-2
tests/admin_email.rs
··· 137 137 .await 138 138 .expect("Failed to send email"); 139 139 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 140 - let body: Value = res.json().await.expect("Invalid JSON"); 141 - assert_eq!(body["error"], "InvalidRequest"); 142 140 } 143 141 144 142 #[tokio::test]
+1 -1
tests/delete_account.rs
··· 414 414 .expect("Failed to send delete request"); 415 415 assert_eq!(delete_res.status(), StatusCode::BAD_REQUEST); 416 416 let body: Value = delete_res.json().await.unwrap(); 417 - assert_eq!(body["error"], "AccountNotFound"); 417 + assert_eq!(body["error"], "InvalidRequest"); 418 418 }
+2 -2
tests/import_verification.rs
··· 102 102 assert_eq!(import_res.status(), StatusCode::FORBIDDEN); 103 103 let body: serde_json::Value = import_res.json().await.unwrap(); 104 104 assert!( 105 - body["error"] == "InvalidRequest" || body["error"] == "DidMismatch", 106 - "Expected DidMismatch or InvalidRequest error, got: {:?}", 105 + body["error"] == "InvalidRepo" || body["error"] == "InvalidRequest" || body["error"] == "DidMismatch", 106 + "Expected InvalidRepo, DidMismatch, or InvalidRequest error, got: {:?}", 107 107 body 108 108 ); 109 109 }
+1 -1
tests/lifecycle_record.rs
··· 771 771 .send() 772 772 .await 773 773 .expect("Failed with nonexistent repo"); 774 - assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND); 774 + assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); 775 775 }