+15
-9
.sqlx/query-0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd.json
.sqlx/query-d4e4c9de4330cc017f457eaec4195b0cf35607d2d0ef6b73e9bb5e94e7742e7a.json
+15
-9
.sqlx/query-0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd.json
.sqlx/query-d4e4c9de4330cc017f457eaec4195b0cf35607d2d0ef6b73e9bb5e94e7742e7a.json
···
1
{
2
"db_name": "PostgreSQL",
3
-
"query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
4
"describe": {
5
"columns": [
6
{
···
25
},
26
{
27
"ordinal": 4,
28
-
"name": "deactivated_at",
29
-
"type_info": "Timestamptz"
30
},
31
{
32
"ordinal": 5,
33
-
"name": "preferred_locale",
34
-
"type_info": "Varchar"
35
},
36
{
37
"ordinal": 6,
38
"name": "preferred_channel: crate::comms::CommsChannel",
39
"type_info": {
40
"Custom": {
···
51
}
52
},
53
{
54
-
"ordinal": 7,
55
"name": "discord_verified",
56
"type_info": "Bool"
57
},
58
{
59
-
"ordinal": 8,
60
"name": "telegram_verified",
61
"type_info": "Bool"
62
},
63
{
64
-
"ordinal": 9,
65
"name": "signal_verified",
66
"type_info": "Bool"
67
}
···
78
false,
79
true,
80
true,
81
false,
82
false,
83
false,
84
false
85
]
86
},
87
-
"hash": "0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd"
88
}
···
1
{
2
"db_name": "PostgreSQL",
3
+
"query": "SELECT\n handle, email, email_verified, is_admin, preferred_locale, deactivated_at, takedown_ref,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
4
"describe": {
5
"columns": [
6
{
···
25
},
26
{
27
"ordinal": 4,
28
+
"name": "preferred_locale",
29
+
"type_info": "Varchar"
30
},
31
{
32
"ordinal": 5,
33
+
"name": "deactivated_at",
34
+
"type_info": "Timestamptz"
35
},
36
{
37
"ordinal": 6,
38
+
"name": "takedown_ref",
39
+
"type_info": "Text"
40
+
},
41
+
{
42
+
"ordinal": 7,
43
"name": "preferred_channel: crate::comms::CommsChannel",
44
"type_info": {
45
"Custom": {
···
56
}
57
},
58
{
59
+
"ordinal": 8,
60
"name": "discord_verified",
61
"type_info": "Bool"
62
},
63
{
64
+
"ordinal": 9,
65
"name": "telegram_verified",
66
"type_info": "Bool"
67
},
68
{
69
+
"ordinal": 10,
70
"name": "signal_verified",
71
"type_info": "Bool"
72
}
···
83
false,
84
true,
85
true,
86
+
true,
87
false,
88
false,
89
false,
90
false
91
]
92
},
93
+
"hash": "d4e4c9de4330cc017f457eaec4195b0cf35607d2d0ef6b73e9bb5e94e7742e7a"
94
}
+18
-6
.sqlx/query-7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c.json
.sqlx/query-c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782.json
+18
-6
.sqlx/query-7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c.json
.sqlx/query-c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782.json
···
1
{
2
"db_name": "PostgreSQL",
3
-
"query": "SELECT\n handle, email, email_verified, is_admin, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
4
"describe": {
5
"columns": [
6
{
···
25
},
26
{
27
"ordinal": 4,
28
"name": "preferred_locale",
29
"type_info": "Varchar"
30
},
31
{
32
-
"ordinal": 5,
33
"name": "preferred_channel: crate::comms::CommsChannel",
34
"type_info": {
35
"Custom": {
···
46
}
47
},
48
{
49
-
"ordinal": 6,
50
"name": "discord_verified",
51
"type_info": "Bool"
52
},
53
{
54
-
"ordinal": 7,
55
"name": "telegram_verified",
56
"type_info": "Bool"
57
},
58
{
59
-
"ordinal": 8,
60
"name": "signal_verified",
61
"type_info": "Bool"
62
}
···
72
false,
73
false,
74
true,
75
false,
76
false,
77
false,
78
false
79
]
80
},
81
-
"hash": "7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c"
82
}
···
1
{
2
"db_name": "PostgreSQL",
3
+
"query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at, takedown_ref, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
4
"describe": {
5
"columns": [
6
{
···
25
},
26
{
27
"ordinal": 4,
28
+
"name": "deactivated_at",
29
+
"type_info": "Timestamptz"
30
+
},
31
+
{
32
+
"ordinal": 5,
33
+
"name": "takedown_ref",
34
+
"type_info": "Text"
35
+
},
36
+
{
37
+
"ordinal": 6,
38
"name": "preferred_locale",
39
"type_info": "Varchar"
40
},
41
{
42
+
"ordinal": 7,
43
"name": "preferred_channel: crate::comms::CommsChannel",
44
"type_info": {
45
"Custom": {
···
56
}
57
},
58
{
59
+
"ordinal": 8,
60
"name": "discord_verified",
61
"type_info": "Bool"
62
},
63
{
64
+
"ordinal": 9,
65
"name": "telegram_verified",
66
"type_info": "Bool"
67
},
68
{
69
+
"ordinal": 10,
70
"name": "signal_verified",
71
"type_info": "Bool"
72
}
···
82
false,
83
false,
84
true,
85
+
true,
86
+
true,
87
false,
88
false,
89
false,
90
false
91
]
92
},
93
+
"hash": "c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782"
94
}
-22
.sqlx/query-e223898d53602c1c8b23eb08a4b96cf20ac349d1fa4e91334b225d3069209dcf.json
-22
.sqlx/query-e223898d53602c1c8b23eb08a4b96cf20ac349d1fa4e91334b225d3069209dcf.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "SELECT handle FROM users WHERE id = $1",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "handle",
9
-
"type_info": "Text"
10
-
}
11
-
],
12
-
"parameters": {
13
-
"Left": [
14
-
"Uuid"
15
-
]
16
-
},
17
-
"nullable": [
18
-
false
19
-
]
20
-
},
21
-
"hash": "e223898d53602c1c8b23eb08a4b96cf20ac349d1fa4e91334b225d3069209dcf"
22
-
}
···
-34
.sqlx/query-e60550cc972a5b0dd7cbdbc20d6ae6439eae3811d488166dca1b41bcc11f81f7.json
-34
.sqlx/query-e60550cc972a5b0dd7cbdbc20d6ae6439eae3811d488166dca1b41bcc11f81f7.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "SELECT id, handle, deactivated_at FROM users WHERE did = $1",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "id",
9
-
"type_info": "Uuid"
10
-
},
11
-
{
12
-
"ordinal": 1,
13
-
"name": "handle",
14
-
"type_info": "Text"
15
-
},
16
-
{
17
-
"ordinal": 2,
18
-
"name": "deactivated_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": "e60550cc972a5b0dd7cbdbc20d6ae6439eae3811d488166dca1b41bcc11f81f7"
34
-
}
···
+28
-10
.sqlx/query-fe8f204d593dce319bb4624871a3a597ba1d3d9ea32855704b18948fd6bbae38.json
.sqlx/query-1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce.json
+28
-10
.sqlx/query-fe8f204d593dce319bb4624871a3a597ba1d3d9ea32855704b18948fd6bbae38.json
.sqlx/query-1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce.json
···
1
{
2
"db_name": "PostgreSQL",
3
-
"query": "SELECT\n u.id, u.did, u.handle, u.password_hash,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login,\n u.preferred_comms_channel as \"preferred_comms_channel: crate::comms::CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1",
4
"describe": {
5
"columns": [
6
{
···
25
},
26
{
27
"ordinal": 4,
28
"name": "email_verified",
29
"type_info": "Bool"
30
},
31
{
32
-
"ordinal": 5,
33
"name": "discord_verified",
34
"type_info": "Bool"
35
},
36
{
37
-
"ordinal": 6,
38
"name": "telegram_verified",
39
"type_info": "Bool"
40
},
41
{
42
-
"ordinal": 7,
43
"name": "signal_verified",
44
"type_info": "Bool"
45
},
46
{
47
-
"ordinal": 8,
48
"name": "allow_legacy_login",
49
"type_info": "Bool"
50
},
51
{
52
-
"ordinal": 9,
53
"name": "preferred_comms_channel: crate::comms::CommsChannel",
54
"type_info": {
55
"Custom": {
···
66
}
67
},
68
{
69
-
"ordinal": 10,
70
"name": "key_bytes",
71
"type_info": "Bytea"
72
},
73
{
74
-
"ordinal": 11,
75
"name": "encryption_version",
76
"type_info": "Int4"
77
},
78
{
79
-
"ordinal": 12,
80
"name": "totp_enabled",
81
"type_info": "Bool"
82
}
···
91
false,
92
false,
93
true,
94
false,
95
false,
96
false,
···
102
null
103
]
104
},
105
-
"hash": "fe8f204d593dce319bb4624871a3a597ba1d3d9ea32855704b18948fd6bbae38"
106
}
···
1
{
2
"db_name": "PostgreSQL",
3
+
"query": "SELECT\n u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login,\n u.preferred_comms_channel as \"preferred_comms_channel: crate::comms::CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1",
4
"describe": {
5
"columns": [
6
{
···
25
},
26
{
27
"ordinal": 4,
28
+
"name": "email",
29
+
"type_info": "Text"
30
+
},
31
+
{
32
+
"ordinal": 5,
33
+
"name": "deactivated_at",
34
+
"type_info": "Timestamptz"
35
+
},
36
+
{
37
+
"ordinal": 6,
38
+
"name": "takedown_ref",
39
+
"type_info": "Text"
40
+
},
41
+
{
42
+
"ordinal": 7,
43
"name": "email_verified",
44
"type_info": "Bool"
45
},
46
{
47
+
"ordinal": 8,
48
"name": "discord_verified",
49
"type_info": "Bool"
50
},
51
{
52
+
"ordinal": 9,
53
"name": "telegram_verified",
54
"type_info": "Bool"
55
},
56
{
57
+
"ordinal": 10,
58
"name": "signal_verified",
59
"type_info": "Bool"
60
},
61
{
62
+
"ordinal": 11,
63
"name": "allow_legacy_login",
64
"type_info": "Bool"
65
},
66
{
67
+
"ordinal": 12,
68
"name": "preferred_comms_channel: crate::comms::CommsChannel",
69
"type_info": {
70
"Custom": {
···
81
}
82
},
83
{
84
+
"ordinal": 13,
85
"name": "key_bytes",
86
"type_info": "Bytea"
87
},
88
{
89
+
"ordinal": 14,
90
"name": "encryption_version",
91
"type_info": "Int4"
92
},
93
{
94
+
"ordinal": 15,
95
"name": "totp_enabled",
96
"type_info": "Bool"
97
}
···
106
false,
107
false,
108
true,
109
+
true,
110
+
true,
111
+
true,
112
false,
113
false,
114
false,
···
120
null
121
]
122
},
123
+
"hash": "1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce"
124
}
+123
-52
src/api/server/session.rs
+123
-52
src/api/server/session.rs
···
43
}
44
45
#[derive(Deserialize)]
46
pub struct CreateSessionInput {
47
pub identifier: String,
48
pub password: String,
49
}
50
51
#[derive(Serialize)]
···
55
pub refresh_jwt: String,
56
pub handle: String,
57
pub did: String,
58
}
59
60
pub async fn create_session(
···
89
);
90
let row = match sqlx::query!(
91
r#"SELECT
92
-
u.id, u.did, u.handle, u.password_hash,
93
u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,
94
u.allow_legacy_login,
95
u.preferred_comms_channel as "preferred_comms_channel: crate::comms::CommsChannel",
···
157
return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into())
158
.into_response();
159
}
160
let is_verified =
161
row.email_verified || row.discord_verified || row.telegram_verified || row.signal_verified;
162
let is_delegated = crate::delegation::is_delegated_account(&state.db, &row.did)
···
207
return ApiError::InternalError.into_response();
208
}
209
};
210
-
if let Err(e) = sqlx::query!(
211
-
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified, scope, controller_did) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
212
-
row.did,
213
-
access_meta.jti,
214
-
refresh_meta.jti,
215
-
access_meta.expires_at,
216
-
refresh_meta.expires_at,
217
-
is_legacy_login,
218
-
false,
219
-
app_password_scopes,
220
-
app_password_controller
221
-
)
222
-
.execute(&state.db)
223
-
.await
224
-
{
225
error!("Failed to insert session: {:?}", e);
226
return ApiError::InternalError.into_response();
227
}
···
245
}
246
}
247
let handle = full_handle(&row.handle, &pds_hostname);
248
Json(CreateSessionOutput {
249
access_jwt: access_meta.token,
250
refresh_jwt: refresh_meta.token,
251
handle,
252
did: row.did,
253
})
254
.into_response()
255
}
···
261
let permissions = auth_user.permissions();
262
let can_read_email = permissions.allows_email_read();
263
264
-
match sqlx::query!(
265
-
r#"SELECT
266
-
handle, email, email_verified, is_admin, deactivated_at, preferred_locale,
267
-
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
268
-
discord_verified, telegram_verified, signal_verified
269
-
FROM users WHERE did = $1"#,
270
-
auth_user.did
271
-
)
272
-
.fetch_optional(&state.db)
273
-
.await
274
-
{
275
Ok(Some(row)) => {
276
let (preferred_channel, preferred_channel_verified) = match row.preferred_channel {
277
crate::comms::CommsChannel::Email => ("email", row.email_verified),
···
282
let pds_hostname =
283
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
284
let handle = full_handle(&row.handle, &pds_hostname);
285
-
let is_active = row.deactivated_at.is_none();
286
let email_value = if can_read_email {
287
row.email.clone()
288
} else {
289
None
290
};
291
-
let email_verified_value = can_read_email && row.email_verified;
292
-
Json(json!({
293
"handle": handle,
294
"did": auth_user.did,
295
-
"email": email_value,
296
-
"emailVerified": email_verified_value,
297
"preferredChannel": preferred_channel,
298
"preferredChannelVerified": preferred_channel_verified,
299
"preferredLocale": row.preferred_locale,
300
-
"isAdmin": row.is_admin,
301
-
"active": is_active,
302
-
"status": if is_active { "active" } else { "deactivated" },
303
-
"didDoc": {}
304
-
}))
305
.into_response()
306
}
307
Ok(None) => ApiError::AuthenticationFailed.into_response(),
···
498
error!("Failed to commit transaction: {:?}", e);
499
return ApiError::InternalError.into_response();
500
}
501
-
match sqlx::query!(
502
-
r#"SELECT
503
-
handle, email, email_verified, is_admin, preferred_locale,
504
-
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
505
-
discord_verified, telegram_verified, signal_verified
506
-
FROM users WHERE did = $1"#,
507
-
session_row.did
508
-
)
509
-
.fetch_optional(&state.db)
510
-
.await
511
-
{
512
Ok(Some(u)) => {
513
let (preferred_channel, preferred_channel_verified) = match u.preferred_channel {
514
crate::comms::CommsChannel::Email => ("email", u.email_verified),
···
519
let pds_hostname =
520
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
521
let handle = full_handle(&u.handle, &pds_hostname);
522
-
Json(json!({
523
"accessJwt": new_access_meta.token,
524
"refreshJwt": new_refresh_meta.token,
525
"handle": handle,
526
"did": session_row.did,
527
"email": u.email,
528
-
"emailVerified": u.email_verified,
529
"preferredChannel": preferred_channel,
530
"preferredChannelVerified": preferred_channel_verified,
531
"preferredLocale": u.preferred_locale,
532
"isAdmin": u.is_admin,
533
-
"active": true
534
-
}))
535
.into_response()
536
}
537
Ok(None) => {
···
43
}
44
45
#[derive(Deserialize)]
46
+
#[serde(rename_all = "camelCase")]
47
pub struct CreateSessionInput {
48
pub identifier: String,
49
pub password: String,
50
+
#[serde(default)]
51
+
pub allow_takendown: bool,
52
}
53
54
#[derive(Serialize)]
···
58
pub refresh_jwt: String,
59
pub handle: String,
60
pub did: String,
61
+
#[serde(skip_serializing_if = "Option::is_none")]
62
+
pub did_doc: Option<serde_json::Value>,
63
+
#[serde(skip_serializing_if = "Option::is_none")]
64
+
pub email: Option<String>,
65
+
#[serde(skip_serializing_if = "Option::is_none")]
66
+
pub email_confirmed: Option<bool>,
67
+
#[serde(skip_serializing_if = "Option::is_none")]
68
+
pub active: Option<bool>,
69
+
#[serde(skip_serializing_if = "Option::is_none")]
70
+
pub status: Option<String>,
71
}
72
73
pub async fn create_session(
···
102
);
103
let row = match sqlx::query!(
104
r#"SELECT
105
+
u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,
106
u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,
107
u.allow_legacy_login,
108
u.preferred_comms_channel as "preferred_comms_channel: crate::comms::CommsChannel",
···
170
return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into())
171
.into_response();
172
}
173
+
let is_takendown = row.takedown_ref.is_some();
174
+
if is_takendown && !input.allow_takendown {
175
+
warn!("Login attempt for takendown account: {}", row.did);
176
+
return (
177
+
StatusCode::UNAUTHORIZED,
178
+
Json(json!({
179
+
"error": "AccountTakedown",
180
+
"message": "Account has been taken down"
181
+
})),
182
+
)
183
+
.into_response();
184
+
}
185
let is_verified =
186
row.email_verified || row.discord_verified || row.telegram_verified || row.signal_verified;
187
let is_delegated = crate::delegation::is_delegated_account(&state.db, &row.did)
···
232
return ApiError::InternalError.into_response();
233
}
234
};
235
+
let did_for_doc = row.did.clone();
236
+
let did_resolver = state.did_resolver.clone();
237
+
let (insert_result, did_doc) = tokio::join!(
238
+
sqlx::query!(
239
+
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified, scope, controller_did) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
240
+
row.did,
241
+
access_meta.jti,
242
+
refresh_meta.jti,
243
+
access_meta.expires_at,
244
+
refresh_meta.expires_at,
245
+
is_legacy_login,
246
+
false,
247
+
app_password_scopes,
248
+
app_password_controller
249
+
)
250
+
.execute(&state.db),
251
+
did_resolver.resolve_did_document(&did_for_doc)
252
+
);
253
+
if let Err(e) = insert_result {
254
error!("Failed to insert session: {:?}", e);
255
return ApiError::InternalError.into_response();
256
}
···
274
}
275
}
276
let handle = full_handle(&row.handle, &pds_hostname);
277
+
let is_active = row.deactivated_at.is_none() && !is_takendown;
278
+
let status = if is_takendown {
279
+
Some("takendown".to_string())
280
+
} else if row.deactivated_at.is_some() {
281
+
Some("deactivated".to_string())
282
+
} else {
283
+
None
284
+
};
285
Json(CreateSessionOutput {
286
access_jwt: access_meta.token,
287
refresh_jwt: refresh_meta.token,
288
handle,
289
did: row.did,
290
+
did_doc,
291
+
email: row.email,
292
+
email_confirmed: Some(row.email_verified),
293
+
active: Some(is_active),
294
+
status,
295
})
296
.into_response()
297
}
···
303
let permissions = auth_user.permissions();
304
let can_read_email = permissions.allows_email_read();
305
306
+
let did_for_doc = auth_user.did.clone();
307
+
let did_resolver = state.did_resolver.clone();
308
+
let (db_result, did_doc) = tokio::join!(
309
+
sqlx::query!(
310
+
r#"SELECT
311
+
handle, email, email_verified, is_admin, deactivated_at, takedown_ref, preferred_locale,
312
+
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
313
+
discord_verified, telegram_verified, signal_verified
314
+
FROM users WHERE did = $1"#,
315
+
auth_user.did
316
+
)
317
+
.fetch_optional(&state.db),
318
+
did_resolver.resolve_did_document(&did_for_doc)
319
+
);
320
+
match db_result {
321
Ok(Some(row)) => {
322
let (preferred_channel, preferred_channel_verified) = match row.preferred_channel {
323
crate::comms::CommsChannel::Email => ("email", row.email_verified),
···
328
let pds_hostname =
329
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
330
let handle = full_handle(&row.handle, &pds_hostname);
331
+
let is_takendown = row.takedown_ref.is_some();
332
+
let is_active = row.deactivated_at.is_none() && !is_takendown;
333
let email_value = if can_read_email {
334
row.email.clone()
335
} else {
336
None
337
};
338
+
let email_confirmed_value = can_read_email && row.email_verified;
339
+
let mut response = json!({
340
"handle": handle,
341
"did": auth_user.did,
342
+
"active": is_active,
343
"preferredChannel": preferred_channel,
344
"preferredChannelVerified": preferred_channel_verified,
345
"preferredLocale": row.preferred_locale,
346
+
"isAdmin": row.is_admin
347
+
});
348
+
if can_read_email {
349
+
response["email"] = json!(email_value);
350
+
response["emailConfirmed"] = json!(email_confirmed_value);
351
+
}
352
+
if is_takendown {
353
+
response["status"] = json!("takendown");
354
+
} else if row.deactivated_at.is_some() {
355
+
response["status"] = json!("deactivated");
356
+
}
357
+
if let Some(doc) = did_doc {
358
+
response["didDoc"] = doc;
359
+
}
360
+
Json(response)
361
.into_response()
362
}
363
Ok(None) => ApiError::AuthenticationFailed.into_response(),
···
554
error!("Failed to commit transaction: {:?}", e);
555
return ApiError::InternalError.into_response();
556
}
557
+
let did_for_doc = session_row.did.clone();
558
+
let did_resolver = state.did_resolver.clone();
559
+
let (db_result, did_doc) = tokio::join!(
560
+
sqlx::query!(
561
+
r#"SELECT
562
+
handle, email, email_verified, is_admin, preferred_locale, deactivated_at, takedown_ref,
563
+
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
564
+
discord_verified, telegram_verified, signal_verified
565
+
FROM users WHERE did = $1"#,
566
+
session_row.did
567
+
)
568
+
.fetch_optional(&state.db),
569
+
did_resolver.resolve_did_document(&did_for_doc)
570
+
);
571
+
match db_result {
572
Ok(Some(u)) => {
573
let (preferred_channel, preferred_channel_verified) = match u.preferred_channel {
574
crate::comms::CommsChannel::Email => ("email", u.email_verified),
···
579
let pds_hostname =
580
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
581
let handle = full_handle(&u.handle, &pds_hostname);
582
+
let is_takendown = u.takedown_ref.is_some();
583
+
let is_active = u.deactivated_at.is_none() && !is_takendown;
584
+
let mut response = json!({
585
"accessJwt": new_access_meta.token,
586
"refreshJwt": new_refresh_meta.token,
587
"handle": handle,
588
"did": session_row.did,
589
"email": u.email,
590
+
"emailConfirmed": u.email_verified,
591
"preferredChannel": preferred_channel,
592
"preferredChannelVerified": preferred_channel_verified,
593
"preferredLocale": u.preferred_locale,
594
"isAdmin": u.is_admin,
595
+
"active": is_active
596
+
});
597
+
if let Some(doc) = did_doc {
598
+
response["didDoc"] = doc;
599
+
}
600
+
if is_takendown {
601
+
response["status"] = json!("takendown");
602
+
} else if u.deactivated_at.is_some() {
603
+
response["status"] = json!("deactivated");
604
+
}
605
+
Json(response)
606
.into_response()
607
}
608
Ok(None) => {
+140
-42
src/appview/mod.rs
+140
-42
src/appview/mod.rs
···
29
resolved_at: Instant,
30
}
31
32
#[derive(Debug, Clone)]
33
pub struct ResolvedService {
34
pub url: String,
···
37
38
pub struct DidResolver {
39
did_cache: RwLock<HashMap<String, CachedDid>>,
40
client: Client,
41
cache_ttl: Duration,
42
plc_directory_url: String,
···
46
fn clone(&self) -> Self {
47
Self {
48
did_cache: RwLock::new(HashMap::new()),
49
client: self.client.clone(),
50
cache_ttl: self.cache_ttl,
51
plc_directory_url: self.plc_directory_url.clone(),
···
74
75
Self {
76
did_cache: RwLock::new(HashMap::new()),
77
client,
78
cache_ttl: Duration::from_secs(cache_ttl_secs),
79
plc_directory_url,
80
}
81
}
82
83
pub async fn resolve_did(&self, did: &str) -> Option<ResolvedService> {
84
{
85
let cache = self.did_cache.read().await;
···
140
}
141
142
async fn resolve_did_web(&self, did: &str) -> Result<DidDocument, String> {
143
-
let host = did
144
-
.strip_prefix("did:web:")
145
-
.ok_or("Invalid did:web format")?;
146
-
147
-
let (host, path) = if host.contains(':') {
148
-
let decoded = host.replace("%3A", ":");
149
-
let parts: Vec<&str> = decoded.splitn(2, '/').collect();
150
-
if parts.len() > 1 {
151
-
(parts[0].to_string(), format!("/{}", parts[1]))
152
-
} else {
153
-
(decoded, String::new())
154
-
}
155
-
} else {
156
-
let parts: Vec<&str> = host.splitn(2, ':').collect();
157
-
if parts.len() > 1 && parts[1].contains('/') {
158
-
let path_parts: Vec<&str> = parts[1].splitn(2, '/').collect();
159
-
if path_parts.len() > 1 {
160
-
(
161
-
format!("{}:{}", parts[0], path_parts[0]),
162
-
format!("/{}", path_parts[1]),
163
-
)
164
-
} else {
165
-
(host.to_string(), String::new())
166
-
}
167
-
} else {
168
-
(host.to_string(), String::new())
169
-
}
170
-
};
171
-
172
-
let scheme =
173
-
if host.starts_with("localhost") || host.starts_with("127.0.0.1") || host.contains(':')
174
-
{
175
-
"http"
176
-
} else {
177
-
"https"
178
-
};
179
-
180
-
let url = if path.is_empty() {
181
-
format!("{}://{}/.well-known/did.json", scheme, host)
182
-
} else {
183
-
format!("{}://{}{}/did.json", scheme, host, path)
184
-
};
185
186
debug!("Resolving did:web {} via {}", did, url);
187
···
286
None
287
}
288
289
pub async fn invalidate_cache(&self, did: &str) {
290
let mut cache = self.did_cache.write().await;
291
cache.remove(did);
292
}
293
}
294
···
29
resolved_at: Instant,
30
}
31
32
+
#[derive(Clone)]
33
+
struct CachedDidDocument {
34
+
document: serde_json::Value,
35
+
resolved_at: Instant,
36
+
}
37
+
38
#[derive(Debug, Clone)]
39
pub struct ResolvedService {
40
pub url: String,
···
43
44
pub struct DidResolver {
45
did_cache: RwLock<HashMap<String, CachedDid>>,
46
+
did_doc_cache: RwLock<HashMap<String, CachedDidDocument>>,
47
client: Client,
48
cache_ttl: Duration,
49
plc_directory_url: String,
···
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(),
···
82
83
Self {
84
did_cache: RwLock::new(HashMap::new()),
85
+
did_doc_cache: RwLock::new(HashMap::new()),
86
client,
87
cache_ttl: Duration::from_secs(cache_ttl_secs),
88
plc_directory_url,
89
}
90
}
91
92
+
fn build_did_web_url(did: &str) -> Result<String, String> {
93
+
let host = did
94
+
.strip_prefix("did:web:")
95
+
.ok_or("Invalid did:web format")?;
96
+
97
+
let (host, path) = if host.contains(':') {
98
+
let decoded = host.replace("%3A", ":");
99
+
let parts: Vec<&str> = decoded.splitn(2, '/').collect();
100
+
if parts.len() > 1 {
101
+
(parts[0].to_string(), format!("/{}", parts[1]))
102
+
} else {
103
+
(decoded, String::new())
104
+
}
105
+
} else {
106
+
let parts: Vec<&str> = host.splitn(2, ':').collect();
107
+
if parts.len() > 1 && parts[1].contains('/') {
108
+
let path_parts: Vec<&str> = parts[1].splitn(2, '/').collect();
109
+
if path_parts.len() > 1 {
110
+
(
111
+
format!("{}:{}", parts[0], path_parts[0]),
112
+
format!("/{}", path_parts[1]),
113
+
)
114
+
} else {
115
+
(host.to_string(), String::new())
116
+
}
117
+
} else {
118
+
(host.to_string(), String::new())
119
+
}
120
+
};
121
+
122
+
let scheme =
123
+
if host.starts_with("localhost") || host.starts_with("127.0.0.1") || host.contains(':')
124
+
{
125
+
"http"
126
+
} else {
127
+
"https"
128
+
};
129
+
130
+
let url = if path.is_empty() {
131
+
format!("{}://{}/.well-known/did.json", scheme, host)
132
+
} else {
133
+
format!("{}://{}{}/did.json", scheme, host, path)
134
+
};
135
+
136
+
Ok(url)
137
+
}
138
+
139
pub async fn resolve_did(&self, did: &str) -> Option<ResolvedService> {
140
{
141
let cache = self.did_cache.read().await;
···
196
}
197
198
async fn resolve_did_web(&self, did: &str) -> Result<DidDocument, String> {
199
+
let url = Self::build_did_web_url(did)?;
200
201
debug!("Resolving did:web {} via {}", did, url);
202
···
301
None
302
}
303
304
+
pub async fn resolve_did_document(&self, did: &str) -> Option<serde_json::Value> {
305
+
{
306
+
let cache = self.did_doc_cache.read().await;
307
+
if let Some(cached) = cache.get(did)
308
+
&& cached.resolved_at.elapsed() < self.cache_ttl
309
+
{
310
+
return Some(cached.document.clone());
311
+
}
312
+
}
313
+
314
+
let result = if did.starts_with("did:web:") {
315
+
self.fetch_did_document_web(did).await
316
+
} else if did.starts_with("did:plc:") {
317
+
self.fetch_did_document_plc(did).await
318
+
} else {
319
+
warn!("Unsupported DID method for document resolution: {}", did);
320
+
return None;
321
+
};
322
+
323
+
match result {
324
+
Ok(doc) => {
325
+
let mut cache = self.did_doc_cache.write().await;
326
+
cache.insert(
327
+
did.to_string(),
328
+
CachedDidDocument {
329
+
document: doc.clone(),
330
+
resolved_at: Instant::now(),
331
+
},
332
+
);
333
+
Some(doc)
334
+
}
335
+
Err(e) => {
336
+
warn!("Failed to resolve DID document for {}: {}", did, e);
337
+
None
338
+
}
339
+
}
340
+
}
341
+
342
+
async fn fetch_did_document_web(&self, did: &str) -> Result<serde_json::Value, String> {
343
+
let url = Self::build_did_web_url(did)?;
344
+
345
+
let resp = self
346
+
.client
347
+
.get(&url)
348
+
.send()
349
+
.await
350
+
.map_err(|e| format!("HTTP request failed: {}", e))?;
351
+
352
+
if !resp.status().is_success() {
353
+
return Err(format!("HTTP {}", resp.status()));
354
+
}
355
+
356
+
resp.json::<serde_json::Value>()
357
+
.await
358
+
.map_err(|e| format!("Failed to parse DID document: {}", e))
359
+
}
360
+
361
+
async fn fetch_did_document_plc(&self, did: &str) -> Result<serde_json::Value, String> {
362
+
let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did));
363
+
364
+
let resp = self
365
+
.client
366
+
.get(&url)
367
+
.send()
368
+
.await
369
+
.map_err(|e| format!("HTTP request failed: {}", e))?;
370
+
371
+
if resp.status() == reqwest::StatusCode::NOT_FOUND {
372
+
return Err("DID not found".to_string());
373
+
}
374
+
375
+
if !resp.status().is_success() {
376
+
return Err(format!("HTTP {}", resp.status()));
377
+
}
378
+
379
+
resp.json::<serde_json::Value>()
380
+
.await
381
+
.map_err(|e| format!("Failed to parse DID document: {}", e))
382
+
}
383
+
384
pub async fn invalidate_cache(&self, did: &str) {
385
let mut cache = self.did_cache.write().await;
386
cache.remove(did);
387
+
drop(cache);
388
+
let mut doc_cache = self.did_doc_cache.write().await;
389
+
doc_cache.remove(did);
390
}
391
}
392