+224
-408
Diff
round #0
+3
-7
crates/tranquil-api/src/admin/account/delete.rs
+3
-7
crates/tranquil-api/src/admin/account/delete.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
response::{IntoResponse, Response},
5
-
};
1
+
use axum::{Json, extract::State};
6
2
use serde::Deserialize;
7
3
use tracing::warn;
8
4
use tranquil_pds::api::EmptyResponse;
···
20
16
State(state): State<AppState>,
21
17
_auth: Auth<Admin>,
22
18
Json(input): Json<DeleteAccountInput>,
23
-
) -> Result<Response, ApiError> {
19
+
) -> Result<Json<EmptyResponse>, ApiError> {
24
20
let did = &input.did;
25
21
let (user_id, handle) = state
26
22
.user_repo
···
52
48
.cache
53
49
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
54
50
.await;
55
-
Ok(EmptyResponse::ok().into_response())
51
+
Ok(Json(EmptyResponse {}))
56
52
}
+4
-9
crates/tranquil-api/src/admin/account/email.rs
+4
-9
crates/tranquil-api/src/admin/account/email.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
http::StatusCode,
5
-
response::{IntoResponse, Response},
6
-
};
1
+
use axum::{Json, extract::State};
7
2
use serde::{Deserialize, Serialize};
8
3
use tracing::warn;
9
4
use tranquil_pds::api::error::{ApiError, DbResultExt};
···
30
25
State(state): State<AppState>,
31
26
_auth: Auth<Admin>,
32
27
Json(input): Json<SendEmailInput>,
33
-
) -> Result<Response, ApiError> {
28
+
) -> Result<Json<SendEmailOutput>, ApiError> {
34
29
let content = input.content.trim();
35
30
if content.is_empty() {
36
31
return Err(ApiError::InvalidRequest("content is required".into()));
···
68
63
handle,
69
64
input.recipient_did
70
65
);
71
-
Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response())
66
+
Ok(Json(SendEmailOutput { sent: true }))
72
67
}
73
68
Err(e) => {
74
69
warn!("Failed to enqueue admin email: {:?}", e);
75
-
Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response())
70
+
Ok(Json(SendEmailOutput { sent: false }))
76
71
}
77
72
}
78
73
}
+28
-47
crates/tranquil-api/src/admin/account/info.rs
+28
-47
crates/tranquil-api/src/admin/account/info.rs
···
1
+
use crate::common;
1
2
use axum::{
2
3
Json,
3
4
extract::{Query, RawQuery, State},
4
-
http::StatusCode,
5
-
response::{IntoResponse, Response},
6
5
};
7
6
use serde::{Deserialize, Serialize};
8
7
use std::collections::HashMap;
···
68
67
State(state): State<AppState>,
69
68
_auth: Auth<Admin>,
70
69
Query(params): Query<GetAccountInfoParams>,
71
-
) -> Result<Response, ApiError> {
70
+
) -> Result<Json<AccountInfo>, ApiError> {
72
71
let account = state
73
72
.infra_repo
74
73
.get_admin_account_info_by_did(¶ms.did)
···
79
78
let invited_by = get_invited_by(&state, account.id).await;
80
79
let invites = get_invites_for_user(&state, account.id).await;
81
80
82
-
Ok((
83
-
StatusCode::OK,
84
-
Json(AccountInfo {
85
-
did: account.did,
86
-
handle: account.handle,
87
-
email: account.email,
88
-
indexed_at: account.created_at.to_rfc3339(),
89
-
invite_note: None,
90
-
invites_disabled: account.invites_disabled,
91
-
email_confirmed_at: if account.email_verified {
92
-
Some(account.created_at.to_rfc3339())
93
-
} else {
94
-
None
95
-
},
96
-
deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()),
97
-
invited_by,
98
-
invites,
99
-
}),
100
-
)
101
-
.into_response())
81
+
Ok(Json(AccountInfo {
82
+
did: account.did,
83
+
handle: account.handle,
84
+
email: account.email,
85
+
indexed_at: account.created_at.to_rfc3339(),
86
+
invite_note: None,
87
+
invites_disabled: account.invites_disabled,
88
+
email_confirmed_at: if account.email_verified {
89
+
Some(account.created_at.to_rfc3339())
90
+
} else {
91
+
None
92
+
},
93
+
deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()),
94
+
invited_by,
95
+
invites,
96
+
}))
102
97
}
103
98
104
99
async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> {
···
133
128
.await
134
129
.ok()?;
135
130
136
-
let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> =
137
-
uses.into_iter().fold(HashMap::new(), |mut acc, u| {
138
-
acc.entry(u.code.clone())
139
-
.or_default()
140
-
.push(InviteCodeUseInfo {
141
-
used_by: u.used_by_did,
142
-
used_at: u.used_at.to_rfc3339(),
143
-
});
144
-
acc
145
-
});
131
+
let uses_by_code = common::group_invite_uses_by_code(uses, |u| InviteCodeUseInfo {
132
+
used_by: u.used_by_did,
133
+
used_at: u.used_at.to_rfc3339(),
134
+
});
146
135
147
136
let invites: Vec<InviteCodeInfo> = invite_codes
148
137
.into_iter()
···
195
184
State(state): State<AppState>,
196
185
_auth: Auth<Admin>,
197
186
RawQuery(raw_query): RawQuery,
198
-
) -> Result<Response, ApiError> {
187
+
) -> Result<Json<GetAccountInfosOutput>, ApiError> {
199
188
let dids: Vec<String> =
200
189
tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
201
190
.into_iter()
···
244
233
.into_iter()
245
234
.collect();
246
235
247
-
let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> =
248
-
all_invite_uses
249
-
.into_iter()
250
-
.fold(HashMap::new(), |mut acc, u| {
251
-
acc.entry(u.code.clone())
252
-
.or_default()
253
-
.push(InviteCodeUseInfo {
254
-
used_by: u.used_by_did,
255
-
used_at: u.used_at.to_rfc3339(),
256
-
});
257
-
acc
258
-
});
236
+
let uses_by_code = common::group_invite_uses_by_code(all_invite_uses, |u| InviteCodeUseInfo {
237
+
used_by: u.used_by_did,
238
+
used_at: u.used_at.to_rfc3339(),
239
+
});
259
240
260
241
let (codes_by_user, code_info_map): (
261
242
HashMap<uuid::Uuid, Vec<InviteCodeInfo>>,
···
304
285
})
305
286
.collect();
306
287
307
-
Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response())
288
+
Ok(Json(GetAccountInfosOutput { infos }))
308
289
}
+5
-11
crates/tranquil-api/src/admin/account/search.rs
+5
-11
crates/tranquil-api/src/admin/account/search.rs
···
1
1
use axum::{
2
2
Json,
3
3
extract::{Query, State},
4
-
http::StatusCode,
5
-
response::{IntoResponse, Response},
6
4
};
7
5
use serde::{Deserialize, Serialize};
8
6
use tranquil_pds::api::error::{ApiError, DbResultExt};
···
51
49
State(state): State<AppState>,
52
50
_auth: Auth<Admin>,
53
51
Query(params): Query<SearchAccountsParams>,
54
-
) -> Result<Response, ApiError> {
52
+
) -> Result<Json<SearchAccountsOutput>, ApiError> {
55
53
let limit = params.limit.clamp(1, 100);
56
54
let email_filter = params.email.as_deref().map(|e| format!("%{}%", e));
57
55
let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
···
91
89
} else {
92
90
None
93
91
};
94
-
Ok((
95
-
StatusCode::OK,
96
-
Json(SearchAccountsOutput {
97
-
cursor: next_cursor,
98
-
accounts,
99
-
}),
100
-
)
101
-
.into_response())
92
+
Ok(Json(SearchAccountsOutput {
93
+
cursor: next_cursor,
94
+
accounts,
95
+
}))
102
96
}
+7
-11
crates/tranquil-api/src/admin/account/update.rs
+7
-11
crates/tranquil-api/src/admin/account/update.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
response::{IntoResponse, Response},
5
-
};
1
+
use axum::{Json, extract::State};
6
2
use serde::Deserialize;
7
3
use tracing::{error, warn};
8
4
use tranquil_pds::api::EmptyResponse;
···
21
17
State(state): State<AppState>,
22
18
_auth: Auth<Admin>,
23
19
Json(input): Json<UpdateAccountEmailInput>,
24
-
) -> Result<Response, ApiError> {
20
+
) -> Result<Json<EmptyResponse>, ApiError> {
25
21
let account = input.account.trim();
26
22
let email = input.email.trim();
27
23
if account.is_empty() || email.is_empty() {
···
39
35
.await
40
36
{
41
37
Ok(0) => Err(ApiError::AccountNotFound),
42
-
Ok(_) => Ok(EmptyResponse::ok().into_response()),
38
+
Ok(_) => Ok(Json(EmptyResponse {})),
43
39
Err(e) => {
44
40
error!("DB error updating email: {:?}", e);
45
41
Err(ApiError::InternalError(None))
···
57
53
State(state): State<AppState>,
58
54
_auth: Auth<Admin>,
59
55
Json(input): Json<UpdateAccountHandleInput>,
60
-
) -> Result<Response, ApiError> {
56
+
) -> Result<Json<EmptyResponse>, ApiError> {
61
57
let did = &input.did;
62
58
let input_handle = input.handle.trim();
63
59
if input_handle.is_empty() {
···
125
121
{
126
122
warn!("Failed to update PLC handle for admin handle update: {}", e);
127
123
}
128
-
Ok(EmptyResponse::ok().into_response())
124
+
Ok(Json(EmptyResponse {}))
129
125
}
130
126
Err(e) => {
131
127
error!("DB error updating handle: {:?}", e);
···
144
140
State(state): State<AppState>,
145
141
_auth: Auth<Admin>,
146
142
Json(input): Json<UpdateAccountPasswordInput>,
147
-
) -> Result<Response, ApiError> {
143
+
) -> Result<Json<EmptyResponse>, ApiError> {
148
144
let did = &input.did;
149
145
let password = input.password.trim();
150
146
if password.is_empty() {
···
161
157
.await
162
158
{
163
159
Ok(0) => Err(ApiError::AccountNotFound),
164
-
Ok(_) => Ok(EmptyResponse::ok().into_response()),
160
+
Ok(_) => Ok(Json(EmptyResponse {})),
165
161
Err(e) => {
166
162
error!("DB error updating password: {:?}", e);
167
163
Err(ApiError::InternalError(None))
+6
-6
crates/tranquil-api/src/admin/config.rs
+6
-6
crates/tranquil-api/src/admin/config.rs
···
8
8
9
9
#[derive(Serialize)]
10
10
#[serde(rename_all = "camelCase")]
11
-
pub struct ServerConfigResponse {
11
+
pub struct ServerConfigOutput {
12
12
pub server_name: String,
13
13
pub primary_color: Option<String>,
14
14
pub primary_color_dark: Option<String>,
···
29
29
}
30
30
31
31
#[derive(Serialize)]
32
-
pub struct UpdateServerConfigResponse {
32
+
pub struct UpdateServerConfigOutput {
33
33
pub success: bool,
34
34
}
35
35
···
42
42
43
43
pub async fn get_server_config(
44
44
State(state): State<AppState>,
45
-
) -> Result<Json<ServerConfigResponse>, ApiError> {
45
+
) -> Result<Json<ServerConfigOutput>, ApiError> {
46
46
let keys = &[
47
47
"server_name",
48
48
"primary_color",
···
60
60
61
61
let config_map: std::collections::HashMap<String, String> = rows.into_iter().collect();
62
62
63
-
Ok(Json(ServerConfigResponse {
63
+
Ok(Json(ServerConfigOutput {
64
64
server_name: config_map
65
65
.get("server_name")
66
66
.cloned()
···
77
77
State(state): State<AppState>,
78
78
_auth: Auth<Admin>,
79
79
Json(req): Json<UpdateServerConfigRequest>,
80
-
) -> Result<Json<UpdateServerConfigResponse>, ApiError> {
80
+
) -> Result<Json<UpdateServerConfigOutput>, ApiError> {
81
81
if let Some(server_name) = req.server_name {
82
82
let trimmed = server_name.trim();
83
83
if trimmed.is_empty() || trimmed.len() > 100 {
···
224
224
}
225
225
}
226
226
227
-
Ok(Json(UpdateServerConfigResponse { success: true }))
227
+
Ok(Json(UpdateServerConfigOutput { success: true }))
228
228
}
+23
-33
crates/tranquil-api/src/admin/invite.rs
+23
-33
crates/tranquil-api/src/admin/invite.rs
···
1
+
use crate::common;
1
2
use axum::{
2
3
Json,
3
4
extract::{Query, State},
4
-
http::StatusCode,
5
-
response::{IntoResponse, Response},
6
5
};
7
6
use serde::{Deserialize, Serialize};
8
7
use tracing::error;
···
23
22
State(state): State<AppState>,
24
23
_auth: Auth<Admin>,
25
24
Json(input): Json<DisableInviteCodesInput>,
26
-
) -> Result<Response, ApiError> {
25
+
) -> Result<Json<EmptyResponse>, ApiError> {
27
26
if let Some(codes) = &input.codes
28
27
&& let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await
29
28
{
···
40
39
error!("DB error disabling invite codes by account: {:?}", e);
41
40
}
42
41
}
43
-
Ok(EmptyResponse::ok().into_response())
42
+
Ok(Json(EmptyResponse {}))
44
43
}
45
44
46
45
#[derive(Deserialize)]
···
80
79
State(state): State<AppState>,
81
80
_auth: Auth<Admin>,
82
81
Query(params): Query<GetInviteCodesParams>,
83
-
) -> Result<Response, ApiError> {
82
+
) -> Result<Json<GetInviteCodesOutput>, ApiError> {
84
83
let limit = params.limit.unwrap_or(100).clamp(1, 500);
85
84
let sort_order = match params.sort.as_deref() {
86
85
Some("usage") => InviteCodeSortOrder::Usage,
···
104
103
.into_iter()
105
104
.collect();
106
105
107
-
let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> =
108
-
if code_strings.is_empty() {
109
-
std::collections::HashMap::new()
110
-
} else {
106
+
let uses_by_code = if code_strings.is_empty() {
107
+
std::collections::HashMap::new()
108
+
} else {
109
+
common::group_invite_uses_by_code(
111
110
state
112
111
.infra_repo
113
112
.get_invite_code_uses_batch(&code_strings)
114
113
.await
115
-
.unwrap_or_default()
116
-
.into_iter()
117
-
.fold(std::collections::HashMap::new(), |mut acc, u| {
118
-
acc.entry(u.code.clone())
119
-
.or_default()
120
-
.push(InviteCodeUseInfo {
121
-
used_by: u.used_by_did.to_string(),
122
-
used_at: u.used_at.to_rfc3339(),
123
-
});
124
-
acc
125
-
})
126
-
};
114
+
.unwrap_or_default(),
115
+
|u| InviteCodeUseInfo {
116
+
used_by: u.used_by_did.to_string(),
117
+
used_at: u.used_at.to_rfc3339(),
118
+
},
119
+
)
120
+
};
127
121
128
122
let codes: Vec<InviteCodeInfo> = codes_rows
129
123
.iter()
···
149
143
} else {
150
144
None
151
145
};
152
-
Ok((
153
-
StatusCode::OK,
154
-
Json(GetInviteCodesOutput {
155
-
cursor: next_cursor,
156
-
codes,
157
-
}),
158
-
)
159
-
.into_response())
146
+
Ok(Json(GetInviteCodesOutput {
147
+
cursor: next_cursor,
148
+
codes,
149
+
}))
160
150
}
161
151
162
152
#[derive(Deserialize)]
···
168
158
State(state): State<AppState>,
169
159
_auth: Auth<Admin>,
170
160
Json(input): Json<DisableAccountInvitesInput>,
171
-
) -> Result<Response, ApiError> {
161
+
) -> Result<Json<EmptyResponse>, ApiError> {
172
162
let account = input.account.trim();
173
163
if account.is_empty() {
174
164
return Err(ApiError::InvalidRequest("account is required".into()));
···
182
172
.set_invites_disabled(&account_did, true)
183
173
.await
184
174
{
185
-
Ok(true) => Ok(EmptyResponse::ok().into_response()),
175
+
Ok(true) => Ok(Json(EmptyResponse {})),
186
176
Ok(false) => Err(ApiError::AccountNotFound),
187
177
Err(e) => {
188
178
error!("DB error disabling account invites: {:?}", e);
···
200
190
State(state): State<AppState>,
201
191
_auth: Auth<Admin>,
202
192
Json(input): Json<EnableAccountInvitesInput>,
203
-
) -> Result<Response, ApiError> {
193
+
) -> Result<Json<EmptyResponse>, ApiError> {
204
194
let account = input.account.trim();
205
195
if account.is_empty() {
206
196
return Err(ApiError::InvalidRequest("account is required".into()));
···
214
204
.set_invites_disabled(&account_did, false)
215
205
.await
216
206
{
217
-
Ok(true) => Ok(EmptyResponse::ok().into_response()),
207
+
Ok(true) => Ok(Json(EmptyResponse {})),
218
208
Ok(false) => Err(ApiError::AccountNotFound),
219
209
Err(e) => {
220
210
error!("DB error enabling account invites: {:?}", e);
+5
-10
crates/tranquil-api/src/admin/server_stats.rs
+5
-10
crates/tranquil-api/src/admin/server_stats.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
response::{IntoResponse, Response},
5
-
};
1
+
use axum::{Json, extract::State};
6
2
use serde::Serialize;
7
3
use tranquil_pds::api::error::ApiError;
8
4
use tranquil_pds::auth::{Admin, Auth};
···
10
6
11
7
#[derive(Serialize)]
12
8
#[serde(rename_all = "camelCase")]
13
-
pub struct ServerStatsResponse {
9
+
pub struct ServerStatsOutput {
14
10
pub user_count: i64,
15
11
pub repo_count: i64,
16
12
pub record_count: i64,
···
20
16
pub async fn get_server_stats(
21
17
State(state): State<AppState>,
22
18
_auth: Auth<Admin>,
23
-
) -> Result<Response, ApiError> {
19
+
) -> Result<Json<ServerStatsOutput>, ApiError> {
24
20
let user_count = state.user_repo.count_users().await.unwrap_or(0);
25
21
let repo_count = state.repo_repo.count_repos().await.unwrap_or(0);
26
22
let record_count = state.repo_repo.count_all_records().await.unwrap_or(0);
27
23
let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0);
28
24
29
-
Ok(Json(ServerStatsResponse {
25
+
Ok(Json(ServerStatsOutput {
30
26
user_count,
31
27
repo_count,
32
28
record_count,
33
29
blob_storage_bytes,
34
-
})
35
-
.into_response())
30
+
}))
36
31
}
+53
-79
crates/tranquil-api/src/admin/status.rs
+53
-79
crates/tranquil-api/src/admin/status.rs
···
1
1
use axum::{
2
2
Json,
3
3
extract::{Query, State},
4
-
http::StatusCode,
5
-
response::{IntoResponse, Response},
6
4
};
7
5
use serde::{Deserialize, Serialize};
8
-
use serde_json::json;
6
+
use serde_json::{Value, json};
9
7
use tracing::{error, warn};
10
8
use tranquil_pds::api::error::ApiError;
11
9
use tranquil_pds::auth::{Admin, Auth};
···
37
35
State(state): State<AppState>,
38
36
_auth: Auth<Admin>,
39
37
Query(params): Query<GetSubjectStatusParams>,
40
-
) -> Result<Response, ApiError> {
38
+
) -> Result<Json<SubjectStatus>, ApiError> {
41
39
if params.did.is_none() && params.uri.is_none() && params.blob.is_none() {
42
40
return Err(ApiError::InvalidRequest(
43
41
"Must provide did, uri, or blob".into(),
···
57
55
applied: true,
58
56
r#ref: Some(r.clone()),
59
57
});
60
-
return Ok((
61
-
StatusCode::OK,
62
-
Json(SubjectStatus {
63
-
subject: json!({
64
-
"$type": "com.atproto.admin.defs#repoRef",
65
-
"did": did_str
66
-
}),
67
-
takedown,
68
-
deactivated,
58
+
return Ok(Json(SubjectStatus {
59
+
subject: json!({
60
+
"$type": "com.atproto.admin.defs#repoRef",
61
+
"did": did_str
69
62
}),
70
-
)
71
-
.into_response());
63
+
takedown,
64
+
deactivated,
65
+
}));
72
66
}
73
67
Ok(None) => {
74
68
return Err(ApiError::SubjectNotFound);
···
89
83
applied: true,
90
84
r#ref: Some(r.clone()),
91
85
});
92
-
return Ok((
93
-
StatusCode::OK,
94
-
Json(SubjectStatus {
95
-
subject: json!({
96
-
"$type": "com.atproto.repo.strongRef",
97
-
"uri": uri_str,
98
-
"cid": uri_str
99
-
}),
100
-
takedown,
101
-
deactivated: None,
86
+
return Ok(Json(SubjectStatus {
87
+
subject: json!({
88
+
"$type": "com.atproto.repo.strongRef",
89
+
"uri": uri_str,
90
+
"cid": uri_str
102
91
}),
103
-
)
104
-
.into_response());
92
+
takedown,
93
+
deactivated: None,
94
+
}));
105
95
}
106
96
Ok(None) => {
107
97
return Err(ApiError::RecordNotFound);
···
125
115
applied: true,
126
116
r#ref: Some(r.clone()),
127
117
});
128
-
return Ok((
129
-
StatusCode::OK,
130
-
Json(SubjectStatus {
131
-
subject: json!({
132
-
"$type": "com.atproto.admin.defs#repoBlobRef",
133
-
"did": did,
134
-
"cid": blob.cid
135
-
}),
136
-
takedown,
137
-
deactivated: None,
118
+
return Ok(Json(SubjectStatus {
119
+
subject: json!({
120
+
"$type": "com.atproto.admin.defs#repoBlobRef",
121
+
"did": did,
122
+
"cid": blob.cid
138
123
}),
139
-
)
140
-
.into_response());
124
+
takedown,
125
+
deactivated: None,
126
+
}));
141
127
}
142
128
Ok(None) => {
143
129
return Err(ApiError::BlobNotFound(None));
···
169
155
State(state): State<AppState>,
170
156
_auth: Auth<Admin>,
171
157
Json(input): Json<UpdateSubjectStatusInput>,
172
-
) -> Result<Response, ApiError> {
173
-
let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
158
+
) -> Result<Json<serde_json::Value>, ApiError> {
159
+
let subject_type = input.subject.get("$type").and_then(Value::as_str);
174
160
match subject_type {
175
161
Some("com.atproto.admin.defs#repoRef") => {
176
-
let did_str = input.subject.get("did").and_then(|d| d.as_str());
162
+
let did_str = input.subject.get("did").and_then(Value::as_str);
177
163
if let Some(did_str) = did_str {
178
164
let did: Did = match did_str.parse() {
179
165
Ok(d) => d,
···
238
224
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
239
225
.await;
240
226
}
241
-
return Ok((
242
-
StatusCode::OK,
243
-
Json(json!({
244
-
"subject": input.subject,
245
-
"takedown": input.takedown.as_ref().map(|t| json!({
246
-
"applied": t.applied,
247
-
"ref": t.r#ref
248
-
})),
249
-
"deactivated": input.deactivated.as_ref().map(|d| json!({
250
-
"applied": d.applied
251
-
}))
227
+
return Ok(Json(json!({
228
+
"subject": input.subject,
229
+
"takedown": input.takedown.as_ref().map(|t| json!({
230
+
"applied": t.applied,
231
+
"ref": t.r#ref
252
232
})),
253
-
)
254
-
.into_response());
233
+
"deactivated": input.deactivated.as_ref().map(|d| json!({
234
+
"applied": d.applied
235
+
}))
236
+
})));
255
237
}
256
238
}
257
239
Some("com.atproto.repo.strongRef") => {
258
-
let uri_str = input.subject.get("uri").and_then(|u| u.as_str());
240
+
let uri_str = input.subject.get("uri").and_then(Value::as_str);
259
241
if let Some(uri_str) = uri_str {
260
242
let cid: CidLink = uri_str
261
243
.parse()
···
278
260
ApiError::InternalError(Some("Failed to update takedown status".into()))
279
261
})?;
280
262
}
281
-
return Ok((
282
-
StatusCode::OK,
283
-
Json(json!({
284
-
"subject": input.subject,
285
-
"takedown": input.takedown.as_ref().map(|t| json!({
286
-
"applied": t.applied,
287
-
"ref": t.r#ref
288
-
}))
289
-
})),
290
-
)
291
-
.into_response());
263
+
return Ok(Json(json!({
264
+
"subject": input.subject,
265
+
"takedown": input.takedown.as_ref().map(|t| json!({
266
+
"applied": t.applied,
267
+
"ref": t.r#ref
268
+
}))
269
+
})));
292
270
}
293
271
}
294
272
Some("com.atproto.admin.defs#repoBlobRef") => {
295
-
let cid_str = input.subject.get("cid").and_then(|c| c.as_str());
273
+
let cid_str = input.subject.get("cid").and_then(Value::as_str);
296
274
if let Some(cid_str) = cid_str {
297
275
let cid: CidLink = cid_str
298
276
.parse()
···
315
293
ApiError::InternalError(Some("Failed to update takedown status".into()))
316
294
})?;
317
295
}
318
-
return Ok((
319
-
StatusCode::OK,
320
-
Json(json!({
321
-
"subject": input.subject,
322
-
"takedown": input.takedown.as_ref().map(|t| json!({
323
-
"applied": t.applied,
324
-
"ref": t.r#ref
325
-
}))
326
-
})),
327
-
)
328
-
.into_response());
296
+
return Ok(Json(json!({
297
+
"subject": input.subject,
298
+
"takedown": input.takedown.as_ref().map(|t| json!({
299
+
"applied": t.applied,
300
+
"ref": t.r#ref
301
+
}))
302
+
})));
329
303
}
330
304
}
331
305
_ => {}
+83
-185
crates/tranquil-api/src/identity/did.rs
+83
-185
crates/tranquil-api/src/identity/did.rs
···
1
+
use crate::common;
1
2
use axum::{
2
3
Json,
3
4
extract::{Path, Query, State},
···
10
11
use serde::{Deserialize, Serialize};
11
12
use serde_json::json;
12
13
use tracing::{error, warn};
14
+
use tranquil_pds::api::error::DbResultExt;
13
15
use tranquil_pds::api::{ApiError, DidResponse, EmptyResponse};
14
16
use tranquil_pds::auth::{Auth, NotTakendown};
15
17
use tranquil_pds::plc::signing_key_to_did_key;
···
188
190
189
191
let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname));
190
192
191
-
if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| {
192
-
serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone())
193
-
.ok()
194
-
.filter(|p| !p.is_empty())
195
-
.map(|p| (ovr, p))
196
-
}) {
197
-
let also_known_as = if !ovr.also_known_as.is_empty() {
198
-
ovr.also_known_as.clone()
199
-
} else {
200
-
vec![format!("at://{}", current_handle)]
201
-
};
202
-
203
-
return Json(json!({
204
-
"@context": [
205
-
"https://www.w3.org/ns/did/v1",
206
-
"https://w3id.org/security/multikey/v1",
207
-
"https://w3id.org/security/suites/secp256k1-2019/v1"
208
-
],
209
-
"id": did,
210
-
"alsoKnownAs": also_known_as,
211
-
"verificationMethod": parsed.iter().map(|m| json!({
212
-
"id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }),
213
-
"type": m.method_type,
214
-
"controller": did,
215
-
"publicKeyMultibase": m.public_key_multibase
216
-
})).collect::<Vec<_>>(),
217
-
"service": [{
218
-
"id": "#atproto_pds",
219
-
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
220
-
"serviceEndpoint": service_endpoint
221
-
}]
222
-
}))
223
-
.into_response();
224
-
}
225
-
226
-
let key_info = match state.user_repo.get_user_key_by_id(user_id).await {
227
-
Ok(Some(k)) => k,
228
-
Ok(None) => return ApiError::InternalError(None).into_response(),
229
-
Err(_) => return ApiError::InternalError(None).into_response(),
230
-
};
231
-
let key_bytes: Vec<u8> =
232
-
match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
233
-
Ok(k) => k,
234
-
Err(_) => {
235
-
return ApiError::InternalError(None).into_response();
236
-
}
237
-
};
238
-
let public_key_multibase = match get_public_key_multibase(&key_bytes) {
239
-
Ok(pk) => pk,
240
-
Err(e) => {
241
-
tracing::error!("Failed to generate public key multibase: {}", e);
242
-
return ApiError::InternalError(None).into_response();
243
-
}
244
-
};
245
-
246
-
let also_known_as = if let Some(ref ovr) = overrides {
247
-
if !ovr.also_known_as.is_empty() {
248
-
ovr.also_known_as.clone()
249
-
} else {
250
-
vec![format!("at://{}", current_handle)]
251
-
}
252
-
} else {
253
-
vec![format!("at://{}", current_handle)]
193
+
let verification_methods =
194
+
build_override_or_key_verification_methods(state, user_id, &did, overrides.as_ref()).await;
195
+
let verification_methods = match verification_methods {
196
+
Ok(vm) => vm,
197
+
Err(resp) => return resp,
254
198
};
255
199
256
-
Json(json!({
257
-
"@context": [
258
-
"https://www.w3.org/ns/did/v1",
259
-
"https://w3id.org/security/multikey/v1",
260
-
"https://w3id.org/security/suites/secp256k1-2019/v1"
261
-
],
262
-
"id": did,
263
-
"alsoKnownAs": also_known_as,
264
-
"verificationMethod": [{
265
-
"id": format!("{}#atproto", did),
266
-
"type": "Multikey",
267
-
"controller": did,
268
-
"publicKeyMultibase": public_key_multibase
269
-
}],
270
-
"service": [{
271
-
"id": "#atproto_pds",
272
-
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
273
-
"serviceEndpoint": service_endpoint
274
-
}]
275
-
}))
200
+
let also_known_as = common::resolve_also_known_as(overrides.as_ref(), ¤t_handle);
201
+
Json(common::build_did_document(
202
+
&did,
203
+
also_known_as,
204
+
verification_methods,
205
+
&service_endpoint,
206
+
))
276
207
.into_response()
277
208
}
278
209
···
323
254
324
255
let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname));
325
256
326
-
if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| {
257
+
let verification_methods =
258
+
build_override_or_key_verification_methods(&state, user_id, &did, overrides.as_ref()).await;
259
+
let verification_methods = match verification_methods {
260
+
Ok(vm) => vm,
261
+
Err(resp) => return resp,
262
+
};
263
+
264
+
let also_known_as = common::resolve_also_known_as(overrides.as_ref(), ¤t_handle);
265
+
Json(common::build_did_document(
266
+
&did,
267
+
also_known_as,
268
+
verification_methods,
269
+
&service_endpoint,
270
+
))
271
+
.into_response()
272
+
}
273
+
274
+
async fn build_override_or_key_verification_methods(
275
+
state: &AppState,
276
+
user_id: uuid::Uuid,
277
+
did: &str,
278
+
overrides: Option<&tranquil_db_traits::DidWebOverrides>,
279
+
) -> Result<Vec<serde_json::Value>, Response> {
280
+
if let Some(parsed) = overrides.and_then(|ovr| {
327
281
serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone())
328
282
.ok()
329
283
.filter(|p| !p.is_empty())
330
-
.map(|p| (ovr, p))
331
284
}) {
332
-
let also_known_as = if !ovr.also_known_as.is_empty() {
333
-
ovr.also_known_as.clone()
334
-
} else {
335
-
vec![format!("at://{}", current_handle)]
336
-
};
337
-
338
-
return Json(json!({
339
-
"@context": [
340
-
"https://www.w3.org/ns/did/v1",
341
-
"https://w3id.org/security/multikey/v1",
342
-
"https://w3id.org/security/suites/secp256k1-2019/v1"
343
-
],
344
-
"id": did,
345
-
"alsoKnownAs": also_known_as,
346
-
"verificationMethod": parsed.iter().map(|m| json!({
347
-
"id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }),
348
-
"type": m.method_type,
349
-
"controller": did,
350
-
"publicKeyMultibase": m.public_key_multibase
351
-
})).collect::<Vec<_>>(),
352
-
"service": [{
353
-
"id": "#atproto_pds",
354
-
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
355
-
"serviceEndpoint": service_endpoint
356
-
}]
357
-
}))
358
-
.into_response();
285
+
return Ok(parsed
286
+
.iter()
287
+
.map(|m| {
288
+
json!({
289
+
"id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }),
290
+
"type": m.method_type,
291
+
"controller": did,
292
+
"publicKeyMultibase": m.public_key_multibase
293
+
})
294
+
})
295
+
.collect());
359
296
}
360
297
361
298
let key_info = match state.user_repo.get_user_key_by_id(user_id).await {
362
299
Ok(Some(k)) => k,
363
-
Ok(None) => return ApiError::InternalError(None).into_response(),
364
-
Err(_) => return ApiError::InternalError(None).into_response(),
365
-
};
366
-
let key_bytes: Vec<u8> =
367
-
match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
368
-
Ok(k) => k,
369
-
Err(_) => {
370
-
return ApiError::InternalError(None).into_response();
371
-
}
372
-
};
373
-
let public_key_multibase = match get_public_key_multibase(&key_bytes) {
374
-
Ok(pk) => pk,
375
-
Err(e) => {
376
-
tracing::error!("Failed to generate public key multibase: {}", e);
377
-
return ApiError::InternalError(None).into_response();
378
-
}
379
-
};
380
-
381
-
let also_known_as = if let Some(ref ovr) = overrides {
382
-
if !ovr.also_known_as.is_empty() {
383
-
ovr.also_known_as.clone()
384
-
} else {
385
-
vec![format!("at://{}", current_handle)]
386
-
}
387
-
} else {
388
-
vec![format!("at://{}", current_handle)]
300
+
_ => return Err(ApiError::InternalError(None).into_response()),
389
301
};
302
+
let key_bytes =
303
+
tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version)
304
+
.map_err(|_| ApiError::InternalError(None).into_response())?;
305
+
let public_key_multibase = get_public_key_multibase(&key_bytes).map_err(|e| {
306
+
tracing::error!("Failed to generate public key multibase: {}", e);
307
+
ApiError::InternalError(None).into_response()
308
+
})?;
390
309
391
-
Json(json!({
392
-
"@context": [
393
-
"https://www.w3.org/ns/did/v1",
394
-
"https://w3id.org/security/multikey/v1",
395
-
"https://w3id.org/security/suites/secp256k1-2019/v1"
396
-
],
397
-
"id": did,
398
-
"alsoKnownAs": also_known_as,
399
-
"verificationMethod": [{
400
-
"id": format!("{}#atproto", did),
401
-
"type": "Multikey",
402
-
"controller": did,
403
-
"publicKeyMultibase": public_key_multibase
404
-
}],
405
-
"service": [{
406
-
"id": "#atproto_pds",
407
-
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
408
-
"serviceEndpoint": service_endpoint
409
-
}]
410
-
}))
411
-
.into_response()
310
+
Ok(vec![json!({
311
+
"id": format!("{}#atproto", did),
312
+
"type": "Multikey",
313
+
"controller": did,
314
+
"publicKeyMultibase": public_key_multibase
315
+
})])
412
316
}
413
317
414
318
#[derive(Debug, thiserror::Error)]
···
562
466
pub async fn get_recommended_did_credentials(
563
467
State(state): State<AppState>,
564
468
auth: Auth<NotTakendown>,
565
-
) -> Result<Response, ApiError> {
469
+
) -> Result<Json<GetRecommendedDidCredentialsOutput>, ApiError> {
566
470
let handle = state
567
471
.user_repo
568
472
.get_handle_by_did(&auth.did)
569
473
.await
570
-
.map_err(|_| ApiError::InternalError(None))?
474
+
.log_db_err("fetching handle for DID credentials")?
571
475
.ok_or(ApiError::InternalError(None))?;
572
476
573
477
let key_bytes = auth.key_bytes.clone().ok_or_else(|| {
···
593
497
};
594
498
vec![server_rotation_key]
595
499
};
596
-
Ok((
597
-
StatusCode::OK,
598
-
Json(GetRecommendedDidCredentialsOutput {
599
-
rotation_keys,
600
-
also_known_as: vec![format!("at://{}", handle)],
601
-
verification_methods: VerificationMethods { atproto: did_key },
602
-
services: Services {
603
-
atproto_pds: AtprotoPds {
604
-
service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(),
605
-
endpoint: pds_endpoint,
606
-
},
500
+
Ok(Json(GetRecommendedDidCredentialsOutput {
501
+
rotation_keys,
502
+
also_known_as: vec![format!("at://{}", handle)],
503
+
verification_methods: VerificationMethods { atproto: did_key },
504
+
services: Services {
505
+
atproto_pds: AtprotoPds {
506
+
service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(),
507
+
endpoint: pds_endpoint,
607
508
},
608
-
}),
609
-
)
610
-
.into_response())
509
+
},
510
+
}))
611
511
}
612
512
613
513
#[derive(Deserialize)]
···
619
519
State(state): State<AppState>,
620
520
auth: Auth<NotTakendown>,
621
521
Json(input): Json<UpdateHandleInput>,
622
-
) -> Result<Response, ApiError> {
623
-
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
522
+
) -> Result<Json<EmptyResponse>, ApiError> {
523
+
tranquil_pds::auth::scope_check::check_identity_scope(
624
524
&auth.auth_source,
625
525
auth.scope.as_deref(),
626
526
tranquil_pds::oauth::scopes::IdentityAttr::Handle,
627
-
) {
628
-
return Ok(e);
629
-
}
527
+
)?;
630
528
let did = auth.did.clone();
631
529
let _rate_limit = check_user_rate_limit_with_message::<HandleUpdateLimit>(
632
530
&state,
···
644
542
.user_repo
645
543
.get_id_and_handle_by_did(&did)
646
544
.await
647
-
.map_err(|_| ApiError::InternalError(None))?
545
+
.log_db_err("fetching user for handle update")?
648
546
.ok_or(ApiError::InternalError(None))?;
649
547
let user_id = user_row.id;
650
548
let current_handle = user_row.handle;
···
710
608
{
711
609
warn!("Failed to sequence identity event for handle update: {}", e);
712
610
}
713
-
return Ok(EmptyResponse::ok().into_response());
611
+
return Ok(Json(EmptyResponse {}));
714
612
}
715
613
if short_part.contains('.') {
716
614
return Err(ApiError::InvalidHandle(Some(
···
736
634
{
737
635
warn!("Failed to sequence identity event for handle update: {}", e);
738
636
}
739
-
return Ok(EmptyResponse::ok().into_response());
637
+
return Ok(Json(EmptyResponse {}));
740
638
}
741
639
match tranquil_pds::handle::verify_handle_ownership(&new_handle, &did).await {
742
640
Ok(()) => {}
···
766
664
.user_repo
767
665
.check_handle_exists(&handle_typed, user_id)
768
666
.await
769
-
.map_err(|_| ApiError::InternalError(None))?;
667
+
.log_db_err("checking handle existence")?;
770
668
if handle_exists {
771
669
return Err(ApiError::HandleTaken);
772
670
}
···
797
695
if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await {
798
696
warn!("Failed to update PLC handle: {}", e);
799
697
}
800
-
Ok(EmptyResponse::ok().into_response())
698
+
Ok(Json(EmptyResponse {}))
801
699
}
802
700
803
701
pub async fn update_plc_handle(
+7
-10
crates/tranquil-pds/src/delegation/roles.rs
+7
-10
crates/tranquil-pds/src/delegation/roles.rs
···
1
1
use std::marker::PhantomData;
2
2
3
-
use axum::response::{IntoResponse, Response};
4
-
5
3
use crate::api::error::ApiError;
6
4
use crate::auth::AuthenticatedUser;
7
5
use crate::state::AppState;
···
29
27
did: &Did,
30
28
check_is_delegated: bool,
31
29
error_msg: &str,
32
-
) -> Result<bool, Response> {
30
+
) -> Result<bool, ApiError> {
33
31
let result = if check_is_delegated {
34
32
state.delegation_repo.is_delegated_account(did).await
35
33
} else {
36
34
state.delegation_repo.controls_any_accounts(did).await
37
35
};
38
36
match result {
39
-
Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into()).into_response()),
37
+
Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into())),
40
38
Ok(false) => Ok(false),
41
39
Err(e) => {
42
40
tracing::error!("Failed to check delegation status: {:?}", e);
43
-
Err(
44
-
ApiError::InternalError(Some("Failed to verify delegation status".into()))
45
-
.into_response(),
46
-
)
41
+
Err(ApiError::InternalError(Some(
42
+
"Failed to verify delegation status".into(),
43
+
)))
47
44
}
48
45
}
49
46
}
···
51
48
pub async fn verify_can_add_controllers<'a>(
52
49
state: &AppState,
53
50
user: &'a AuthenticatedUser,
54
-
) -> Result<CanAddControllers<'a>, Response> {
51
+
) -> Result<CanAddControllers<'a>, ApiError> {
55
52
check_delegation_flag(
56
53
state,
57
54
&user.did,
···
68
65
pub async fn verify_can_control_accounts<'a>(
69
66
state: &AppState,
70
67
user: &'a AuthenticatedUser,
71
-
) -> Result<CanControlAccounts<'a>, Response> {
68
+
) -> Result<CanControlAccounts<'a>, ApiError> {
72
69
check_delegation_flag(
73
70
state,
74
71
&user.did,
History
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
refactor(api): centralize DID document building, update admin endpoints
expand 0 comments
pull request successfully merged