+29
.sqlx/query-032ac69a52c0baa269988f662516a54823770aee565f4cf5da2fc1f9b89b6bbb.json
+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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
+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
+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
+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
+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
+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
+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
+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
+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) = ¶ms.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 ¶ms.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
+1
-1
src/api/age_assurance.rs
+41
-165
src/api/backup.rs
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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(¤t_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
+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(¤t_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
+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
+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
+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(¤t_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(¤t_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
+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
+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
+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
+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
+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
+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
+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, ®_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
+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, ®_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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
}
+2
-1
src/oauth/endpoints/delegation.rs
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
-2
tests/admin_email.rs
+1
-1
tests/delete_account.rs
+1
-1
tests/delete_account.rs
+2
-2
tests/import_verification.rs
+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
}