+200
-392
Diff
round #0
+7
-12
crates/tranquil-api/src/server/app_password.rs
+7
-12
crates/tranquil-api/src/server/app_password.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, Serialize};
7
3
use serde_json::json;
8
4
use tracing::error;
···
34
30
pub async fn list_app_passwords(
35
31
State(state): State<AppState>,
36
32
auth: Auth<Permissive>,
37
-
) -> Result<Response, ApiError> {
33
+
) -> Result<Json<ListAppPasswordsOutput>, ApiError> {
38
34
let user = state
39
35
.user_repo
40
36
.get_by_did(&auth.did)
···
60
56
.map(|d| d.to_string()),
61
57
})
62
58
.collect();
63
-
Ok(Json(ListAppPasswordsOutput { passwords }).into_response())
59
+
Ok(Json(ListAppPasswordsOutput { passwords }))
64
60
}
65
61
66
62
#[derive(Deserialize)]
···
86
82
_rate_limit: RateLimited<AppPasswordLimit>,
87
83
auth: Auth<NotTakendown>,
88
84
Json(input): Json<CreateAppPasswordInput>,
89
-
) -> Result<Response, ApiError> {
85
+
) -> Result<Json<CreateAppPasswordOutput>, ApiError> {
90
86
let user = state
91
87
.user_repo
92
88
.get_by_did(&auth.did)
···
194
190
created_at: created_at.to_rfc3339(),
195
191
privileged: privilege.is_privileged(),
196
192
scopes: final_scopes,
197
-
})
198
-
.into_response())
193
+
}))
199
194
}
200
195
201
196
#[derive(Deserialize)]
···
207
202
State(state): State<AppState>,
208
203
auth: Auth<Permissive>,
209
204
Json(input): Json<RevokeAppPasswordInput>,
210
-
) -> Result<Response, ApiError> {
205
+
) -> Result<Json<EmptyResponse>, ApiError> {
211
206
let user = state
212
207
.user_repo
213
208
.get_by_did(&auth.did)
···
247
242
.await
248
243
.log_db_err("revoking app password")?;
249
244
250
-
Ok(EmptyResponse::ok().into_response())
245
+
Ok(Json(EmptyResponse {}))
251
246
}
+7
-12
crates/tranquil-api/src/server/invite.rs
+7
-12
crates/tranquil-api/src/server/invite.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
response::{IntoResponse, Response},
5
-
};
1
+
use axum::{Json, extract::State};
6
2
use rand::Rng;
7
3
use serde::{Deserialize, Serialize};
8
4
use tracing::error;
···
46
42
State(state): State<AppState>,
47
43
auth: Auth<Admin>,
48
44
Json(input): Json<CreateInviteCodeInput>,
49
-
) -> Result<Response, ApiError> {
45
+
) -> Result<Json<CreateInviteCodeOutput>, ApiError> {
50
46
if input.use_count < 1 {
51
47
return Err(ApiError::InvalidRequest(
52
48
"useCount must be at least 1".into(),
···
66
62
.create_invite_code(&code, input.use_count, Some(&for_account))
67
63
.await
68
64
{
69
-
Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()),
65
+
Ok(true) => Ok(Json(CreateInviteCodeOutput { code })),
70
66
Ok(false) => {
71
67
error!("No admin user found to create invite code");
72
68
Err(ApiError::InternalError(None))
···
101
97
State(state): State<AppState>,
102
98
auth: Auth<Admin>,
103
99
Json(input): Json<CreateInviteCodesInput>,
104
-
) -> Result<Response, ApiError> {
100
+
) -> Result<Json<CreateInviteCodesOutput>, ApiError> {
105
101
if input.use_count < 1 {
106
102
return Err(ApiError::InvalidRequest(
107
103
"useCount must be at least 1".into(),
···
147
143
match result {
148
144
Ok(result_codes) => Ok(Json(CreateInviteCodesOutput {
149
145
codes: result_codes,
150
-
})
151
-
.into_response()),
146
+
})),
152
147
Err(e) => {
153
148
error!("DB error creating invite codes: {:?}", e);
154
149
Err(ApiError::InternalError(None))
···
193
188
State(state): State<AppState>,
194
189
auth: Auth<NotTakendown>,
195
190
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
196
-
) -> Result<Response, ApiError> {
191
+
) -> Result<Json<GetAccountInviteCodesOutput>, ApiError> {
197
192
let include_used = params.include_used.unwrap_or(true);
198
193
199
194
let codes_info = state
···
247
242
.await;
248
243
249
244
let codes: Vec<InviteCode> = codes.into_iter().flatten().collect();
250
-
Ok(Json(GetAccountInviteCodesOutput { codes }).into_response())
245
+
Ok(Json(GetAccountInviteCodesOutput { codes }))
251
246
}
+8
-17
crates/tranquil-api/src/server/migration.rs
+8
-17
crates/tranquil-api/src/server/migration.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 serde_json::json;
9
4
use tranquil_pds::api::ApiError;
···
39
34
State(state): State<AppState>,
40
35
auth: Auth<Active>,
41
36
Json(input): Json<UpdateDidDocumentInput>,
42
-
) -> Result<Response, ApiError> {
37
+
) -> Result<Json<UpdateDidDocumentOutput>, ApiError> {
43
38
if !auth.did.starts_with("did:web:") {
44
39
return Err(ApiError::InvalidRequest(
45
40
"DID document updates are only available for did:web accounts".into(),
···
120
115
121
116
tracing::info!("Updated DID document for {}", &auth.did);
122
117
123
-
Ok((
124
-
StatusCode::OK,
125
-
Json(UpdateDidDocumentOutput {
126
-
success: true,
127
-
did_document: did_doc,
128
-
}),
129
-
)
130
-
.into_response())
118
+
Ok(Json(UpdateDidDocumentOutput {
119
+
success: true,
120
+
did_document: did_doc,
121
+
}))
131
122
}
132
123
133
124
pub async fn get_did_document(
134
125
State(state): State<AppState>,
135
126
auth: Auth<Active>,
136
-
) -> Result<Response, ApiError> {
127
+
) -> Result<Json<serde_json::Value>, ApiError> {
137
128
if !auth.did.starts_with("did:web:") {
138
129
return Err(ApiError::InvalidRequest(
139
130
"This endpoint is only available for did:web accounts".into(),
···
142
133
143
134
let did_doc = build_did_document(&state, &auth.did).await;
144
135
145
-
Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response())
136
+
Ok(Json(serde_json::json!({ "didDocument": did_doc })))
146
137
}
147
138
148
139
async fn build_did_document(state: &AppState, did: &tranquil_pds::types::Did) -> serde_json::Value {
+152
-309
crates/tranquil-api/src/server/passkey_account.rs
+152
-309
crates/tranquil-api/src/server/passkey_account.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
http::HeaderMap,
5
-
response::{IntoResponse, Response},
6
-
};
7
-
use bcrypt::{DEFAULT_COST, hash};
1
+
use crate::common;
2
+
use axum::{Json, extract::State, http::HeaderMap};
8
3
use chrono::{Duration, Utc};
9
-
use jacquard_common::types::{integer::LimitedU32, string::Tid};
10
-
use jacquard_repo::{mst::Mst, storage::BlockStore};
11
4
use rand::Rng;
12
5
use serde::{Deserialize, Serialize};
13
6
use serde_json::json;
14
-
use std::sync::Arc;
15
7
use tracing::{debug, error, info, warn};
16
8
use tranquil_db_traits::WebauthnChallengeType;
17
-
use tranquil_pds::api::SuccessResponse;
18
9
use tranquil_pds::api::error::ApiError;
10
+
use tranquil_pds::api::{OptionsResponse, SuccessResponse};
19
11
use tranquil_pds::auth::NormalizedLoginIdentifier;
20
-
use uuid::Uuid;
21
12
22
13
use tranquil_pds::auth::{ServiceTokenVerifier, generate_app_password, is_service_token};
23
14
use tranquil_pds::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited};
24
-
use tranquil_pds::repo_ops::create_signed_commit;
25
15
use tranquil_pds::state::AppState;
26
16
use tranquil_pds::types::{Did, Handle, PlainPassword};
27
17
use tranquil_pds::validation::validate_password;
···
57
47
58
48
#[derive(Serialize)]
59
49
#[serde(rename_all = "camelCase")]
60
-
pub struct CreatePasskeyAccountResponse {
50
+
pub struct CreatePasskeyAccountOutput {
61
51
pub did: Did,
62
52
pub handle: Handle,
63
53
pub setup_token: String,
···
71
61
_rate_limit: RateLimited<AccountCreationLimit>,
72
62
headers: HeaderMap,
73
63
Json(input): Json<CreatePasskeyAccountInput>,
74
-
) -> Response {
64
+
) -> Result<Json<CreatePasskeyAccountOutput>, ApiError> {
75
65
let byod_auth = if let Some(extracted) = tranquil_pds::auth::extract_auth_token_from_header(
76
66
tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION),
77
67
) {
···
91
81
}
92
82
Err(e) => {
93
83
error!("Service token verification failed: {:?}", e);
94
-
return ApiError::AuthenticationFailed(Some(format!(
84
+
return Err(ApiError::AuthenticationFailed(Some(format!(
95
85
"Service token verification failed: {}",
96
86
e
97
-
)))
98
-
.into_response();
87
+
))));
99
88
}
100
89
}
101
90
} else {
···
116
105
let hostname = &cfg.server.hostname;
117
106
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
118
107
Ok(h) => h,
119
-
Err(_) => return ApiError::InvalidHandle(None).into_response(),
108
+
Err(_) => return Err(ApiError::InvalidHandle(None)),
120
109
};
121
110
122
111
let email = input
···
127
116
if let Some(ref email) = email
128
117
&& !tranquil_pds::api::validation::is_valid_email(email)
129
118
{
130
-
return ApiError::InvalidEmail.into_response();
119
+
return Err(ApiError::InvalidEmail);
131
120
}
132
121
133
122
let is_bootstrap = state.bootstrap_invite_code.is_some()
···
136
125
let _validated_invite_code = if is_bootstrap {
137
126
match input.invite_code.as_deref() {
138
127
Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => None,
139
-
_ => return ApiError::InvalidInviteCode.into_response(),
128
+
_ => return Err(ApiError::InvalidInviteCode),
140
129
}
141
130
} else if let Some(ref code) = input.invite_code {
142
131
match state.infra_repo.validate_invite_code(code).await {
143
132
Ok(validated) => Some(validated),
144
-
Err(_) => return ApiError::InvalidInviteCode.into_response(),
133
+
Err(_) => return Err(ApiError::InvalidInviteCode),
145
134
}
146
135
} else {
147
136
let invite_required = tranquil_config::get().server.invite_code_required;
148
137
if invite_required {
149
-
return ApiError::InviteCodeRequired.into_response();
138
+
return Err(ApiError::InviteCodeRequired);
150
139
}
151
140
None
152
141
};
···
154
143
let verification_channel = input
155
144
.verification_channel
156
145
.unwrap_or(tranquil_db_traits::CommsChannel::Email);
157
-
let verification_recipient = match verification_channel {
158
-
tranquil_db_traits::CommsChannel::Email => match &email {
159
-
Some(e) if !e.is_empty() => e.clone(),
160
-
_ => return ApiError::MissingEmail.into_response(),
161
-
},
162
-
tranquil_db_traits::CommsChannel::Discord => match &input.discord_username {
163
-
Some(username) if !username.trim().is_empty() => {
164
-
let clean = username.trim().to_lowercase();
165
-
if !tranquil_pds::api::validation::is_valid_discord_username(&clean) {
166
-
return ApiError::InvalidRequest(
167
-
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
168
-
).into_response();
169
-
}
170
-
clean
171
-
}
172
-
_ => return ApiError::MissingDiscordId.into_response(),
173
-
},
174
-
tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username {
175
-
Some(username) if !username.trim().is_empty() => {
176
-
let clean = username.trim().trim_start_matches('@');
177
-
if !tranquil_pds::api::validation::is_valid_telegram_username(clean) {
178
-
return ApiError::InvalidRequest(
179
-
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
180
-
).into_response();
181
-
}
182
-
clean.to_string()
183
-
}
184
-
_ => return ApiError::MissingTelegramUsername.into_response(),
185
-
},
186
-
tranquil_db_traits::CommsChannel::Signal => match &input.signal_username {
187
-
Some(username) if !username.trim().is_empty() => {
188
-
username.trim().trim_start_matches('@').to_lowercase()
189
-
}
190
-
_ => return ApiError::MissingSignalNumber.into_response(),
146
+
let verification_recipient = match common::extract_verification_recipient(
147
+
verification_channel,
148
+
&common::ChannelInput {
149
+
email: email.as_deref(),
150
+
discord_username: input.discord_username.as_deref(),
151
+
telegram_username: input.telegram_username.as_deref(),
152
+
signal_username: input.signal_username.as_deref(),
191
153
},
154
+
) {
155
+
Ok(r) => r,
156
+
Err(e) => return Err(e),
192
157
};
193
158
194
-
use k256::ecdsa::SigningKey;
195
-
use rand::rngs::OsRng;
196
-
197
159
let pds_endpoint = format!("https://{}", hostname);
198
160
let did_type = input.did_type.as_deref().unwrap_or("plc");
199
161
200
-
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<Uuid>) =
201
-
if let Some(signing_key_did) = &input.signing_key {
202
-
match state
203
-
.infra_repo
204
-
.get_reserved_signing_key(signing_key_did)
205
-
.await
206
-
{
207
-
Ok(Some(reserved)) => (reserved.private_key_bytes, Some(reserved.id)),
208
-
Ok(None) => {
209
-
return ApiError::InvalidSigningKey.into_response();
210
-
}
211
-
Err(e) => {
212
-
error!("Error looking up reserved signing key: {:?}", e);
213
-
return ApiError::InternalError(None).into_response();
214
-
}
215
-
}
216
-
} else {
217
-
let secret_key = k256::SecretKey::random(&mut OsRng);
218
-
(secret_key.to_bytes().to_vec(), None)
162
+
let key_result =
163
+
match crate::identity::provision::resolve_signing_key(&state, input.signing_key.as_deref())
164
+
.await
165
+
{
166
+
Ok(k) => k,
167
+
Err(e) => return Err(e),
219
168
};
220
-
221
-
let secret_key = match SigningKey::from_slice(&secret_key_bytes) {
222
-
Ok(k) => k,
223
-
Err(e) => {
224
-
error!("Error creating signing key: {:?}", e);
225
-
return ApiError::InternalError(None).into_response();
226
-
}
227
-
};
169
+
let secret_key_bytes = key_result.secret_key_bytes;
170
+
let secret_key = key_result.signing_key;
171
+
let reserved_key_id = key_result.reserved_key_id;
228
172
229
173
let did = match did_type {
230
174
"web" => {
231
-
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
232
-
return ApiError::SelfHostedDidWebDisabled.into_response();
233
-
}
234
-
let encoded_handle = handle.replace(':', "%3A");
235
-
let self_hosted_did = format!("did:web:{}", encoded_handle);
175
+
let self_hosted_did = match common::create_self_hosted_did_web(&handle) {
176
+
Ok(d) => d,
177
+
Err(e) => return Err(e),
178
+
};
236
179
info!(did = %self_hosted_did, "Creating self-hosted did:web passkey account");
237
180
self_hosted_did
238
181
}
···
240
183
let d = match &input.did {
241
184
Some(d) if !d.trim().is_empty() => d.trim(),
242
185
_ => {
243
-
return ApiError::InvalidRequest(
186
+
return Err(ApiError::InvalidRequest(
244
187
"External did:web requires the 'did' field to be provided".into(),
245
-
)
246
-
.into_response();
188
+
));
247
189
}
248
190
};
249
191
if !d.starts_with("did:web:") {
250
-
return ApiError::InvalidDid("External DID must be a did:web".into())
251
-
.into_response();
192
+
return Err(ApiError::InvalidDid(
193
+
"External DID must be a did:web".into(),
194
+
));
252
195
}
253
196
if is_byod_did_web {
254
197
if let Some(ref auth_did) = byod_auth
255
198
&& d != auth_did.as_str()
256
199
{
257
-
return ApiError::AuthorizationError(format!(
200
+
return Err(ApiError::AuthorizationError(format!(
258
201
"Service token issuer {} does not match DID {}",
259
202
auth_did, d
260
-
))
261
-
.into_response();
203
+
)));
262
204
}
263
205
info!(did = %d, "Creating external did:web passkey account (BYOD key)");
264
206
} else {
···
270
212
)
271
213
.await
272
214
{
273
-
return ApiError::InvalidDid(e.to_string()).into_response();
215
+
return Err(ApiError::InvalidDid(e.to_string()));
274
216
}
275
217
info!(did = %d, "Creating external did:web passkey account (reserved key)");
276
218
}
···
281
223
if let Some(ref provided_did) = input.did {
282
224
if provided_did.starts_with("did:plc:") {
283
225
if provided_did != auth_did.as_str() {
284
-
return ApiError::AuthorizationError(format!(
226
+
return Err(ApiError::AuthorizationError(format!(
285
227
"Service token issuer {} does not match DID {}",
286
228
auth_did, provided_did
287
-
))
288
-
.into_response();
229
+
)));
289
230
}
290
231
info!(did = %provided_did, "Creating BYOD did:plc passkey account (migration)");
291
232
provided_did.clone()
292
233
} else {
293
-
return ApiError::InvalidRequest(
234
+
return Err(ApiError::InvalidRequest(
294
235
"BYOD migration requires a did:plc or did:web DID".into(),
295
-
)
296
-
.into_response();
236
+
));
297
237
}
298
238
} else {
299
-
return ApiError::InvalidRequest(
239
+
return Err(ApiError::InvalidRequest(
300
240
"BYOD migration requires the 'did' field".into(),
301
-
)
302
-
.into_response();
241
+
));
303
242
}
304
243
} else {
305
244
let rotation_key = tranquil_config::get()
···
317
256
Ok(r) => r,
318
257
Err(e) => {
319
258
error!("Error creating PLC genesis operation: {:?}", e);
320
-
return ApiError::InternalError(Some(
259
+
return Err(ApiError::InternalError(Some(
321
260
"Failed to create PLC operation".into(),
322
-
))
323
-
.into_response();
261
+
)));
324
262
}
325
263
};
326
264
···
331
269
.await
332
270
{
333
271
error!("Failed to submit PLC genesis operation: {:?}", e);
334
-
return ApiError::UpstreamErrorMsg(format!(
272
+
return Err(ApiError::UpstreamErrorMsg(format!(
335
273
"Failed to register DID with PLC directory: {}",
336
274
e
337
-
))
338
-
.into_response();
275
+
)));
339
276
}
340
277
genesis_result.did
341
278
}
···
345
282
info!(did = %did, handle = %handle, "Created DID for passkey-only account");
346
283
347
284
let setup_token = generate_setup_token();
348
-
let setup_token_hash = match hash(&setup_token, DEFAULT_COST) {
349
-
Ok(h) => h,
350
-
Err(e) => {
351
-
error!("Error hashing setup token: {:?}", e);
352
-
return ApiError::InternalError(None).into_response();
353
-
}
354
-
};
285
+
let setup_token_hash = common::hash_or_internal_error(&setup_token)?;
355
286
let setup_expires_at = Utc::now() + Duration::hours(1);
356
287
357
288
let deactivated_at: Option<chrono::DateTime<Utc>> = if is_byod_did_web {
···
360
291
None
361
292
};
362
293
363
-
let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) {
364
-
Ok(bytes) => bytes,
365
-
Err(e) => {
366
-
error!("Error encrypting signing key: {:?}", e);
367
-
return ApiError::InternalError(None).into_response();
368
-
}
369
-
};
370
-
371
-
let mst = Mst::new(Arc::new(state.block_store.clone()));
372
-
let mst_root = match mst.persist().await {
373
-
Ok(c) => c,
374
-
Err(e) => {
375
-
error!("Error persisting MST: {:?}", e);
376
-
return ApiError::InternalError(None).into_response();
377
-
}
378
-
};
379
-
let rev = Tid::now(LimitedU32::MIN);
380
294
let did_typed: Did = match did.parse() {
381
295
Ok(d) => d,
382
-
Err(_) => return ApiError::InternalError(Some("Invalid DID".into())).into_response(),
296
+
Err(_) => return Err(ApiError::InternalError(Some("Invalid DID".into()))),
383
297
};
384
-
let (commit_bytes, _sig) =
385
-
match create_signed_commit(&did_typed, mst_root, rev.as_ref(), None, &secret_key) {
386
-
Ok(result) => result,
387
-
Err(e) => {
388
-
error!("Error creating genesis commit: {:?}", e);
389
-
return ApiError::InternalError(None).into_response();
390
-
}
391
-
};
392
-
let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
393
-
Ok(c) => c,
394
-
Err(e) => {
395
-
error!("Error saving genesis commit: {:?}", e);
396
-
return ApiError::InternalError(None).into_response();
397
-
}
298
+
let repo = match crate::identity::provision::init_genesis_repo(
299
+
&state,
300
+
&did_typed,
301
+
&secret_key,
302
+
&secret_key_bytes,
303
+
)
304
+
.await
305
+
{
306
+
Ok(r) => r,
307
+
Err(e) => return Err(e),
398
308
};
399
-
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
400
309
401
310
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
402
311
Some(json!({
···
409
318
410
319
let handle_typed: Handle = match handle.parse() {
411
320
Ok(h) => h,
412
-
Err(_) => return ApiError::InvalidHandle(None).into_response(),
321
+
Err(_) => return Err(ApiError::InvalidHandle(None)),
413
322
};
323
+
let repo_for_seq = repo.clone();
324
+
let comms = crate::identity::provision::normalize_comms_usernames(
325
+
input.discord_username.as_deref(),
326
+
input.telegram_username.as_deref(),
327
+
input.signal_username.as_deref(),
328
+
);
414
329
let create_input = tranquil_db_traits::CreatePasskeyAccountInput {
415
330
handle: handle_typed.clone(),
416
331
email: email.clone().unwrap_or_default(),
417
332
did: did_typed.clone(),
418
333
preferred_comms_channel: verification_channel,
419
-
discord_username: input
420
-
.discord_username
421
-
.as_deref()
422
-
.map(|s| s.trim().to_lowercase())
423
-
.filter(|s| !s.is_empty()),
424
-
telegram_username: input
425
-
.telegram_username
426
-
.as_deref()
427
-
.map(|s| s.trim().trim_start_matches('@'))
428
-
.filter(|s| !s.is_empty())
429
-
.map(String::from),
430
-
signal_username: input
431
-
.signal_username
432
-
.as_deref()
433
-
.map(|s| s.trim().trim_start_matches('@'))
434
-
.filter(|s| !s.is_empty())
435
-
.map(|s| s.to_lowercase()),
334
+
discord_username: comms.discord,
335
+
telegram_username: comms.telegram,
336
+
signal_username: comms.signal,
436
337
setup_token_hash,
437
338
setup_expires_at,
438
339
deactivated_at,
439
-
encrypted_key_bytes,
340
+
encrypted_key_bytes: repo.encrypted_key_bytes,
440
341
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
441
342
reserved_key_id,
442
-
commit_cid: commit_cid.to_string(),
443
-
repo_rev: rev.as_ref().to_string(),
444
-
genesis_block_cids,
343
+
commit_cid: repo.commit_cid.to_string(),
344
+
repo_rev: repo.repo_rev.clone(),
345
+
genesis_block_cids: repo.genesis_block_cids,
445
346
invite_code: if is_bootstrap {
446
347
None
447
348
} else {
···
453
354
let create_result = match state.user_repo.create_passkey_account(&create_input).await {
454
355
Ok(r) => r,
455
356
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
456
-
return ApiError::HandleNotAvailable(None).into_response();
357
+
return Err(ApiError::HandleNotAvailable(None));
457
358
}
458
359
Err(tranquil_db_traits::CreateAccountError::EmailTaken) => {
459
-
return ApiError::EmailTaken.into_response();
360
+
return Err(ApiError::EmailTaken);
460
361
}
461
362
Err(e) => {
462
363
error!("Error creating passkey account: {:?}", e);
463
-
return ApiError::InternalError(None).into_response();
364
+
return Err(ApiError::InternalError(None));
464
365
}
465
366
};
466
367
let user_id = create_result.user_id;
467
368
468
369
if !is_byod_did_web {
469
-
if let Err(e) =
470
-
tranquil_pds::repo_ops::sequence_identity_event(&state, &did_typed, Some(&handle_typed))
471
-
.await
472
-
{
473
-
warn!("Failed to sequence identity event for {}: {}", did, e);
474
-
}
475
-
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
476
-
&state,
477
-
&did_typed,
478
-
tranquil_db_traits::AccountStatus::Active,
479
-
)
480
-
.await
481
-
{
482
-
warn!("Failed to sequence account event for {}: {}", did, e);
483
-
}
484
-
let profile_record = serde_json::json!({
485
-
"$type": "app.bsky.actor.profile",
486
-
"displayName": handle
487
-
});
488
-
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
370
+
crate::identity::provision::sequence_new_account(
489
371
&state,
490
372
&did_typed,
491
-
&tranquil_pds::types::PROFILE_COLLECTION,
492
-
&tranquil_pds::types::PROFILE_RKEY,
493
-
&profile_record,
373
+
&handle_typed,
374
+
&repo_for_seq,
375
+
&handle,
494
376
)
495
-
.await
496
-
{
497
-
warn!("Failed to create default profile for {}: {}", did, e);
498
-
}
377
+
.await;
499
378
}
500
379
501
-
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
502
-
&did_typed,
503
-
verification_channel,
504
-
&verification_recipient,
505
-
);
506
-
let formatted_token =
507
-
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
508
-
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
509
-
state.user_repo.as_ref(),
510
-
state.infra_repo.as_ref(),
380
+
crate::identity::provision::enqueue_signup_verification(
381
+
&state,
511
382
user_id,
383
+
&did_typed,
512
384
verification_channel,
513
385
&verification_recipient,
514
-
&formatted_token,
515
-
hostname,
516
386
)
517
-
.await
518
-
{
519
-
warn!("Failed to enqueue signup verification: {:?}", e);
520
-
}
387
+
.await;
521
388
522
389
info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion");
523
390
···
553
420
None
554
421
};
555
422
556
-
Json(CreatePasskeyAccountResponse {
423
+
Ok(Json(CreatePasskeyAccountOutput {
557
424
did: did.into(),
558
425
handle: handle.into(),
559
426
setup_token,
560
427
setup_expires_at,
561
428
access_jwt,
562
-
})
563
-
.into_response()
429
+
}))
564
430
}
565
431
566
432
#[derive(Deserialize)]
···
574
440
575
441
#[derive(Serialize)]
576
442
#[serde(rename_all = "camelCase")]
577
-
pub struct CompletePasskeySetupResponse {
443
+
pub struct CompletePasskeySetupOutput {
578
444
pub did: Did,
579
445
pub handle: Handle,
580
446
pub app_password: String,
···
584
450
pub async fn complete_passkey_setup(
585
451
State(state): State<AppState>,
586
452
Json(input): Json<CompletePasskeySetupInput>,
587
-
) -> Response {
453
+
) -> Result<Json<CompletePasskeySetupOutput>, ApiError> {
588
454
let user = match state.user_repo.get_user_for_passkey_setup(&input.did).await {
589
455
Ok(Some(u)) => u,
590
456
Ok(None) => {
591
-
return ApiError::AccountNotFound.into_response();
457
+
return Err(ApiError::AccountNotFound);
592
458
}
593
459
Err(e) => {
594
460
error!("DB error: {:?}", e);
595
-
return ApiError::InternalError(None).into_response();
461
+
return Err(ApiError::InternalError(None));
596
462
}
597
463
};
598
464
599
465
if user.password_required {
600
-
return ApiError::InvalidAccount.into_response();
466
+
return Err(ApiError::InvalidAccount);
601
467
}
602
468
603
469
let token_hash = match &user.recovery_token {
604
470
Some(h) => h,
605
471
None => {
606
-
return ApiError::SetupExpired.into_response();
472
+
return Err(ApiError::SetupExpired);
607
473
}
608
474
};
609
475
610
-
if let Some(expires_at) = user.recovery_token_expires_at
611
-
&& expires_at < Utc::now()
612
-
{
613
-
return ApiError::SetupExpired.into_response();
614
-
}
615
-
616
-
if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) {
617
-
return ApiError::InvalidToken(None).into_response();
618
-
}
476
+
common::validate_token_hash(
477
+
user.recovery_token_expires_at,
478
+
token_hash,
479
+
&input.setup_token,
480
+
ApiError::SetupExpired,
481
+
ApiError::InvalidToken(None),
482
+
)?;
619
483
620
484
let webauthn = &state.webauthn_config;
621
485
···
628
492
Ok(s) => s,
629
493
Err(e) => {
630
494
error!("Error deserializing registration state: {:?}", e);
631
-
return ApiError::InternalError(None).into_response();
495
+
return Err(ApiError::InternalError(None));
632
496
}
633
497
},
634
498
Ok(None) => {
635
-
return ApiError::NoChallengeInProgress.into_response();
499
+
return Err(ApiError::NoChallengeInProgress);
636
500
}
637
501
Err(e) => {
638
502
error!("Error loading registration state: {:?}", e);
639
-
return ApiError::InternalError(None).into_response();
503
+
return Err(ApiError::InternalError(None));
640
504
}
641
505
};
642
506
···
645
509
Ok(c) => c,
646
510
Err(e) => {
647
511
warn!("Failed to parse credential: {:?}", e);
648
-
return ApiError::InvalidCredential.into_response();
512
+
return Err(ApiError::InvalidCredential);
649
513
}
650
514
};
651
515
···
653
517
Ok(sk) => sk,
654
518
Err(e) => {
655
519
warn!("Passkey registration failed: {:?}", e);
656
-
return ApiError::RegistrationFailed.into_response();
520
+
return Err(ApiError::RegistrationFailed);
657
521
}
658
522
};
659
523
···
662
526
Ok(pk) => pk,
663
527
Err(e) => {
664
528
error!("Error serializing security key: {:?}", e);
665
-
return ApiError::InternalError(None).into_response();
529
+
return Err(ApiError::InternalError(None));
666
530
}
667
531
};
668
532
if let Err(e) = state
···
676
540
.await
677
541
{
678
542
error!("Error saving passkey: {:?}", e);
679
-
return ApiError::InternalError(None).into_response();
543
+
return Err(ApiError::InternalError(None));
680
544
}
681
545
682
546
let app_password = generate_app_password();
683
547
let app_password_name = "bsky.app".to_string();
684
-
let password_hash = match hash(&app_password, DEFAULT_COST) {
685
-
Ok(h) => h,
686
-
Err(e) => {
687
-
error!("Error hashing app password: {:?}", e);
688
-
return ApiError::InternalError(None).into_response();
689
-
}
690
-
};
548
+
let password_hash = common::hash_or_internal_error(&app_password)?;
691
549
692
550
let setup_input = tranquil_db_traits::CompletePasskeySetupInput {
693
551
user_id: user.id,
···
697
555
};
698
556
if let Err(e) = state.user_repo.complete_passkey_setup(&setup_input).await {
699
557
error!("Error completing passkey setup: {:?}", e);
700
-
return ApiError::InternalError(None).into_response();
558
+
return Err(ApiError::InternalError(None));
701
559
}
702
560
703
561
let _ = state
···
707
565
708
566
info!(did = %input.did, "Passkey-only account setup completed");
709
567
710
-
Json(CompletePasskeySetupResponse {
568
+
Ok(Json(CompletePasskeySetupOutput {
711
569
did: input.did.clone(),
712
570
handle: user.handle,
713
571
app_password,
714
572
app_password_name,
715
-
})
716
-
.into_response()
573
+
}))
717
574
}
718
575
719
576
pub async fn start_passkey_registration_for_setup(
720
577
State(state): State<AppState>,
721
578
Json(input): Json<StartPasskeyRegistrationInput>,
722
-
) -> Response {
579
+
) -> Result<Json<OptionsResponse<serde_json::Value>>, ApiError> {
723
580
let user = match state.user_repo.get_user_for_passkey_setup(&input.did).await {
724
581
Ok(Some(u)) => u,
725
582
Ok(None) => {
726
-
return ApiError::AccountNotFound.into_response();
583
+
return Err(ApiError::AccountNotFound);
727
584
}
728
585
Err(e) => {
729
586
error!("DB error: {:?}", e);
730
-
return ApiError::InternalError(None).into_response();
587
+
return Err(ApiError::InternalError(None));
731
588
}
732
589
};
733
590
734
591
if user.password_required {
735
-
return ApiError::InvalidAccount.into_response();
592
+
return Err(ApiError::InvalidAccount);
736
593
}
737
594
738
595
let token_hash = match &user.recovery_token {
739
596
Some(h) => h,
740
597
None => {
741
-
return ApiError::SetupExpired.into_response();
598
+
return Err(ApiError::SetupExpired);
742
599
}
743
600
};
744
601
745
-
if let Some(expires_at) = user.recovery_token_expires_at
746
-
&& expires_at < Utc::now()
747
-
{
748
-
return ApiError::SetupExpired.into_response();
749
-
}
750
-
751
-
if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) {
752
-
return ApiError::InvalidToken(None).into_response();
753
-
}
602
+
common::validate_token_hash(
603
+
user.recovery_token_expires_at,
604
+
token_hash,
605
+
&input.setup_token,
606
+
ApiError::SetupExpired,
607
+
ApiError::InvalidToken(None),
608
+
)?;
754
609
755
610
let webauthn = &state.webauthn_config;
756
611
···
776
631
Ok(result) => result,
777
632
Err(e) => {
778
633
error!("Failed to start passkey registration: {:?}", e);
779
-
return ApiError::InternalError(None).into_response();
634
+
return Err(ApiError::InternalError(None));
780
635
}
781
636
};
782
637
···
784
639
Ok(json) => json,
785
640
Err(e) => {
786
641
error!("Failed to serialize registration state: {:?}", e);
787
-
return ApiError::InternalError(None).into_response();
642
+
return Err(ApiError::InternalError(None));
788
643
}
789
644
};
790
645
if let Err(e) = state
···
793
648
.await
794
649
{
795
650
error!("Failed to save registration state: {:?}", e);
796
-
return ApiError::InternalError(None).into_response();
651
+
return Err(ApiError::InternalError(None));
797
652
}
798
653
799
654
let options = serde_json::to_value(&ccr).unwrap_or(json!({}));
800
-
Json(json!({"options": options})).into_response()
655
+
Ok(OptionsResponse::new(options))
801
656
}
802
657
803
658
#[derive(Deserialize)]
···
819
674
State(state): State<AppState>,
820
675
_rate_limit: RateLimited<PasswordResetLimit>,
821
676
Json(input): Json<RequestPasskeyRecoveryInput>,
822
-
) -> Response {
677
+
) -> Result<Json<SuccessResponse>, ApiError> {
823
678
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
824
679
let identifier = input.email.trim().to_lowercase();
825
680
let identifier = identifier.strip_prefix('@').unwrap_or(&identifier);
···
833
688
{
834
689
Ok(Some(u)) if !u.password_required => u,
835
690
_ => {
836
-
return SuccessResponse::ok().into_response();
691
+
return Ok(Json(SuccessResponse { success: true }));
837
692
}
838
693
};
839
694
840
695
let recovery_token = generate_setup_token();
841
-
let recovery_token_hash = match hash(&recovery_token, DEFAULT_COST) {
842
-
Ok(h) => h,
843
-
Err(_) => {
844
-
return ApiError::InternalError(None).into_response();
845
-
}
846
-
};
696
+
let recovery_token_hash = common::hash_or_internal_error(&recovery_token)?;
847
697
let expires_at = Utc::now() + Duration::hours(1);
848
698
849
699
if let Err(e) = state
···
852
702
.await
853
703
{
854
704
error!("Error updating recovery token: {:?}", e);
855
-
return ApiError::InternalError(None).into_response();
705
+
return Err(ApiError::InternalError(None));
856
706
}
857
707
858
708
let hostname = &tranquil_config::get().server.hostname;
···
873
723
.await;
874
724
875
725
info!(did = %user.did, "Passkey recovery requested");
876
-
SuccessResponse::ok().into_response()
726
+
Ok(Json(SuccessResponse { success: true }))
877
727
}
878
728
879
729
#[derive(Deserialize)]
···
887
737
pub async fn recover_passkey_account(
888
738
State(state): State<AppState>,
889
739
Json(input): Json<RecoverPasskeyAccountInput>,
890
-
) -> Response {
740
+
) -> Result<Json<SuccessResponse>, ApiError> {
891
741
if let Err(e) = validate_password(&input.new_password) {
892
-
return ApiError::InvalidRequest(e.to_string()).into_response();
742
+
return Err(ApiError::InvalidRequest(e.to_string()));
893
743
}
894
744
895
745
let user = match state.user_repo.get_user_for_recovery(&input.did).await {
896
746
Ok(Some(u)) => u,
897
747
_ => {
898
-
return ApiError::InvalidRecoveryLink.into_response();
748
+
return Err(ApiError::InvalidRecoveryLink);
899
749
}
900
750
};
901
751
902
752
let token_hash = match &user.recovery_token {
903
753
Some(h) => h,
904
754
None => {
905
-
return ApiError::InvalidRecoveryLink.into_response();
755
+
return Err(ApiError::InvalidRecoveryLink);
906
756
}
907
757
};
908
758
909
-
if let Some(expires_at) = user.recovery_token_expires_at
910
-
&& expires_at < Utc::now()
911
-
{
912
-
return ApiError::RecoveryLinkExpired.into_response();
913
-
}
914
-
915
-
if !bcrypt::verify(&input.recovery_token, token_hash).unwrap_or(false) {
916
-
return ApiError::InvalidRecoveryLink.into_response();
917
-
}
759
+
common::validate_token_hash(
760
+
user.recovery_token_expires_at,
761
+
token_hash,
762
+
&input.recovery_token,
763
+
ApiError::RecoveryLinkExpired,
764
+
ApiError::InvalidRecoveryLink,
765
+
)?;
918
766
919
-
let password_hash = match hash(&input.new_password, DEFAULT_COST) {
920
-
Ok(h) => h,
921
-
Err(_) => {
922
-
return ApiError::InternalError(None).into_response();
923
-
}
924
-
};
767
+
let password_hash = common::hash_or_internal_error(&input.new_password)?;
925
768
926
769
let recover_input = tranquil_db_traits::RecoverPasskeyAccountInput {
927
770
did: input.did.clone(),
···
935
778
Ok(r) => r,
936
779
Err(e) => {
937
780
error!("Error recovering passkey account: {:?}", e);
938
-
return ApiError::InternalError(None).into_response();
781
+
return Err(ApiError::InternalError(None));
939
782
}
940
783
};
941
784
···
957
800
}
958
801
}
959
802
info!(did = %input.did, "Passkey-only account recovered with temporary password");
960
-
SuccessResponse::ok().into_response()
803
+
Ok(Json(SuccessResponse { success: true }))
961
804
}
+18
-30
crates/tranquil-api/src/server/passkeys.rs
+18
-30
crates/tranquil-api/src/server/passkeys.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, Serialize};
7
3
use tracing::{error, info, warn};
8
4
use tranquil_db_traits::WebauthnChallengeType;
···
20
16
21
17
#[derive(Serialize)]
22
18
#[serde(rename_all = "camelCase")]
23
-
pub struct StartRegistrationResponse {
19
+
pub struct StartRegistrationOutput {
24
20
pub options: serde_json::Value,
25
21
}
26
22
···
28
24
State(state): State<AppState>,
29
25
auth: Auth<Active>,
30
26
Json(input): Json<StartRegistrationInput>,
31
-
) -> Result<Response, ApiError> {
27
+
) -> Result<Json<StartRegistrationOutput>, ApiError> {
32
28
let webauthn = &state.webauthn_config;
33
29
34
30
let handle = state
···
73
69
74
70
info!(did = %auth.did, "Passkey registration started");
75
71
76
-
Ok(Json(StartRegistrationResponse { options }).into_response())
72
+
Ok(Json(StartRegistrationOutput { options }))
77
73
}
78
74
79
75
#[derive(Deserialize)]
···
85
81
86
82
#[derive(Serialize)]
87
83
#[serde(rename_all = "camelCase")]
88
-
pub struct FinishRegistrationResponse {
84
+
pub struct FinishRegistrationOutput {
89
85
pub id: String,
90
86
pub credential_id: String,
91
87
}
···
94
90
State(state): State<AppState>,
95
91
auth: Auth<Active>,
96
92
Json(input): Json<FinishRegistrationInput>,
97
-
) -> Result<Response, ApiError> {
93
+
) -> Result<Json<FinishRegistrationOutput>, ApiError> {
98
94
let webauthn = &state.webauthn_config;
99
95
100
96
let reg_state_json = state
···
154
150
155
151
info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered");
156
152
157
-
Ok(Json(FinishRegistrationResponse {
153
+
Ok(Json(FinishRegistrationOutput {
158
154
id: passkey_id.to_string(),
159
155
credential_id: credential_id_base64,
160
-
})
161
-
.into_response())
156
+
}))
162
157
}
163
158
164
159
#[derive(Serialize)]
···
173
168
174
169
#[derive(Serialize)]
175
170
#[serde(rename_all = "camelCase")]
176
-
pub struct ListPasskeysResponse {
171
+
pub struct ListPasskeysOutput {
177
172
pub passkeys: Vec<PasskeyInfo>,
178
173
}
179
174
180
175
pub async fn list_passkeys(
181
176
State(state): State<AppState>,
182
177
auth: Auth<Active>,
183
-
) -> Result<Response, ApiError> {
178
+
) -> Result<Json<ListPasskeysOutput>, ApiError> {
184
179
let passkeys = state
185
180
.user_repo
186
181
.get_passkeys_for_user(&auth.did)
···
198
193
})
199
194
.collect();
200
195
201
-
Ok(Json(ListPasskeysResponse {
196
+
Ok(Json(ListPasskeysOutput {
202
197
passkeys: passkey_infos,
203
-
})
204
-
.into_response())
198
+
}))
205
199
}
206
200
207
201
#[derive(Deserialize)]
···
214
208
State(state): State<AppState>,
215
209
auth: Auth<Active>,
216
210
Json(input): Json<DeletePasskeyInput>,
217
-
) -> Result<Response, ApiError> {
218
-
let session_mfa = match require_legacy_session_mfa(&state, &auth).await {
219
-
Ok(proof) => proof,
220
-
Err(response) => return Ok(response),
221
-
};
211
+
) -> Result<Json<EmptyResponse>, ApiError> {
212
+
let session_mfa = require_legacy_session_mfa(&state, &auth).await?;
222
213
223
-
let reauth_mfa = match require_reauth_window(&state, &auth).await {
224
-
Ok(proof) => proof,
225
-
Err(response) => return Ok(response),
226
-
};
214
+
let reauth_mfa = require_reauth_window(&state, &auth).await?;
227
215
228
216
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
229
217
230
218
match state.user_repo.delete_passkey(id, reauth_mfa.did()).await {
231
219
Ok(true) => {
232
220
info!(did = %session_mfa.did(), passkey_id = %id, "Passkey deleted");
233
-
Ok(EmptyResponse::ok().into_response())
221
+
Ok(Json(EmptyResponse {}))
234
222
}
235
223
Ok(false) => Err(ApiError::PasskeyNotFound),
236
224
Err(e) => {
···
251
239
State(state): State<AppState>,
252
240
auth: Auth<Active>,
253
241
Json(input): Json<UpdatePasskeyInput>,
254
-
) -> Result<Response, ApiError> {
242
+
) -> Result<Json<EmptyResponse>, ApiError> {
255
243
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
256
244
257
245
match state
···
261
249
{
262
250
Ok(true) => {
263
251
info!(did = %auth.did, passkey_id = %id, "Passkey renamed");
264
-
Ok(EmptyResponse::ok().into_response())
252
+
Ok(Json(EmptyResponse {}))
265
253
}
266
254
Ok(false) => Err(ApiError::PasskeyNotFound),
267
255
Err(e) => {
+8
-12
crates/tranquil-api/src/server/trusted_devices.rs
+8
-12
crates/tranquil-api/src/server/trusted_devices.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
response::{IntoResponse, Response},
5
-
};
1
+
use axum::{Json, extract::State};
6
2
use chrono::{DateTime, Duration, Utc};
7
3
use serde::{Deserialize, Serialize};
8
4
use tracing::{error, info};
···
67
63
68
64
#[derive(Serialize)]
69
65
#[serde(rename_all = "camelCase")]
70
-
pub struct ListTrustedDevicesResponse {
66
+
pub struct ListTrustedDevicesOutput {
71
67
pub devices: Vec<TrustedDevice>,
72
68
}
73
69
74
70
pub async fn list_trusted_devices(
75
71
State(state): State<AppState>,
76
72
auth: Auth<Active>,
77
-
) -> Result<Response, ApiError> {
73
+
) -> Result<Json<ListTrustedDevicesOutput>, ApiError> {
78
74
let rows = state
79
75
.oauth_repo
80
76
.list_trusted_devices(&auth.did)
···
97
93
})
98
94
.collect();
99
95
100
-
Ok(Json(ListTrustedDevicesResponse { devices }).into_response())
96
+
Ok(Json(ListTrustedDevicesOutput { devices }))
101
97
}
102
98
103
99
#[derive(Deserialize)]
···
110
106
State(state): State<AppState>,
111
107
auth: Auth<Active>,
112
108
Json(input): Json<RevokeTrustedDeviceInput>,
113
-
) -> Result<Response, ApiError> {
109
+
) -> Result<Json<SuccessResponse>, ApiError> {
114
110
match state
115
111
.oauth_repo
116
112
.device_belongs_to_user(&input.device_id, &auth.did)
···
133
129
.log_db_err("revoking device trust")?;
134
130
135
131
info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked");
136
-
Ok(SuccessResponse::ok().into_response())
132
+
Ok(Json(SuccessResponse { success: true }))
137
133
}
138
134
139
135
#[derive(Deserialize)]
···
147
143
State(state): State<AppState>,
148
144
auth: Auth<Active>,
149
145
Json(input): Json<UpdateTrustedDeviceInput>,
150
-
) -> Result<Response, ApiError> {
146
+
) -> Result<Json<SuccessResponse>, ApiError> {
151
147
match state
152
148
.oauth_repo
153
149
.device_belongs_to_user(&input.device_id, &auth.did)
···
170
166
.log_db_err("updating device friendly name")?;
171
167
172
168
info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated");
173
-
Ok(SuccessResponse::ok().into_response())
169
+
Ok(Json(SuccessResponse { success: true }))
174
170
}
175
171
176
172
pub async fn get_device_trust_state(
History
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
refactor(api): simplify passkey account creation and auth-adjacent server endpoints
expand 0 comments
pull request successfully merged