+472
-390
Diff
round #0
+206
-341
crates/tranquil-api/src/identity/account.rs
+206
-341
crates/tranquil-api/src/identity/account.rs
···
1
1
use super::did::verify_did_web;
2
+
use crate::common;
2
3
use axum::{
3
4
Json,
4
5
extract::State,
···
6
7
response::{IntoResponse, Response},
7
8
};
8
9
use bcrypt::{DEFAULT_COST, hash};
9
-
use k256::{SecretKey, ecdsa::SigningKey};
10
-
use rand::rngs::OsRng;
11
10
use serde::{Deserialize, Serialize};
12
11
use serde_json::json;
13
-
use tracing::{debug, error, info, warn};
12
+
use tracing::{debug, error, info};
14
13
use tranquil_pds::api::error::ApiError;
15
14
use tranquil_pds::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token};
16
15
use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};
···
47
46
pub verification_channel: tranquil_db_traits::CommsChannel,
48
47
}
49
48
49
+
async fn try_reactivate_migration(
50
+
state: &AppState,
51
+
did: &str,
52
+
handle: &str,
53
+
email: &Option<String>,
54
+
verification_channel: tranquil_db_traits::CommsChannel,
55
+
verification_recipient: Option<&str>,
56
+
) -> Option<Response> {
57
+
let did_typed: Did = match did.parse() {
58
+
Ok(d) => d,
59
+
Err(_) => return Some(ApiError::InternalError(Some("Invalid DID".into())).into_response()),
60
+
};
61
+
let handle_typed: Handle = match handle.parse() {
62
+
Ok(h) => h,
63
+
Err(_) => return Some(ApiError::InvalidHandle(None).into_response()),
64
+
};
65
+
let reactivate_input = tranquil_db_traits::MigrationReactivationInput {
66
+
did: did_typed.clone(),
67
+
new_handle: handle_typed.clone(),
68
+
new_email: email.clone(),
69
+
};
70
+
match state
71
+
.user_repo
72
+
.reactivate_migration_account(&reactivate_input)
73
+
.await
74
+
{
75
+
Ok(reactivated) => {
76
+
info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration");
77
+
let secret_key_bytes = match state
78
+
.user_repo
79
+
.get_user_key_by_id(reactivated.user_id)
80
+
.await
81
+
{
82
+
Ok(Some(key_info)) => {
83
+
match tranquil_pds::config::decrypt_key(
84
+
&key_info.key_bytes,
85
+
key_info.encryption_version,
86
+
) {
87
+
Ok(k) => k,
88
+
Err(e) => {
89
+
error!("Error decrypting key for reactivated account: {:?}", e);
90
+
return Some(ApiError::InternalError(None).into_response());
91
+
}
92
+
}
93
+
}
94
+
_ => {
95
+
error!("No signing key found for reactivated account");
96
+
return Some(
97
+
ApiError::InternalError(Some("Account signing key not found".into()))
98
+
.into_response(),
99
+
);
100
+
}
101
+
};
102
+
let access_meta =
103
+
match tranquil_pds::auth::create_access_token_with_metadata(did, &secret_key_bytes)
104
+
{
105
+
Ok(m) => m,
106
+
Err(e) => {
107
+
error!("Error creating access token: {:?}", e);
108
+
return Some(ApiError::InternalError(None).into_response());
109
+
}
110
+
};
111
+
let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(
112
+
did,
113
+
&secret_key_bytes,
114
+
) {
115
+
Ok(m) => m,
116
+
Err(e) => {
117
+
error!("Error creating refresh token: {:?}", e);
118
+
return Some(ApiError::InternalError(None).into_response());
119
+
}
120
+
};
121
+
let session_data = tranquil_db_traits::SessionTokenCreate {
122
+
did: did_typed.clone(),
123
+
access_jti: access_meta.jti.clone(),
124
+
refresh_jti: refresh_meta.jti.clone(),
125
+
access_expires_at: access_meta.expires_at,
126
+
refresh_expires_at: refresh_meta.expires_at,
127
+
login_type: tranquil_db_traits::LoginType::Modern,
128
+
mfa_verified: false,
129
+
scope: Some("transition:generic transition:chat.bsky".to_string()),
130
+
controller_did: None,
131
+
app_password_name: None,
132
+
};
133
+
if let Err(e) = state.session_repo.create_session(&session_data).await {
134
+
error!("Error creating session: {:?}", e);
135
+
return Some(ApiError::InternalError(None).into_response());
136
+
}
137
+
let verification_required = match verification_recipient {
138
+
Some(recipient) => {
139
+
super::provision::enqueue_migration_verification(
140
+
state,
141
+
reactivated.user_id,
142
+
&did_typed,
143
+
verification_channel,
144
+
recipient,
145
+
)
146
+
.await;
147
+
true
148
+
}
149
+
None => false,
150
+
};
151
+
Some(
152
+
(
153
+
StatusCode::OK,
154
+
Json(CreateAccountOutput {
155
+
handle: handle.to_string().into(),
156
+
did: did_typed.clone(),
157
+
did_doc: state.did_resolver.resolve_did_document(did).await,
158
+
access_jwt: access_meta.token,
159
+
refresh_jwt: refresh_meta.token,
160
+
verification_required,
161
+
verification_channel,
162
+
}),
163
+
)
164
+
.into_response(),
165
+
)
166
+
}
167
+
Err(tranquil_db_traits::MigrationReactivationError::NotFound) => None,
168
+
Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated) => {
169
+
Some(ApiError::AccountAlreadyExists.into_response())
170
+
}
171
+
Err(tranquil_db_traits::MigrationReactivationError::HandleTaken) => {
172
+
Some(ApiError::HandleTaken.into_response())
173
+
}
174
+
Err(e) => {
175
+
error!("Error reactivating migration account: {:?}", e);
176
+
Some(ApiError::InternalError(None).into_response())
177
+
}
178
+
}
179
+
}
180
+
50
181
pub async fn create_account(
51
182
State(state): State<AppState>,
52
183
_rate_limit: RateLimited<AccountCreationLimit>,
···
154
285
.verification_channel
155
286
.unwrap_or(tranquil_db_traits::CommsChannel::Email);
156
287
let verification_recipient = {
157
-
Some(match verification_channel {
158
-
tranquil_db_traits::CommsChannel::Email => match &input.email {
159
-
Some(email) if !email.trim().is_empty() => email.trim().to_string(),
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(),
288
+
Some(
289
+
match common::extract_verification_recipient(
290
+
verification_channel,
291
+
&common::ChannelInput {
292
+
email: input.email.as_deref(),
293
+
discord_username: input.discord_username.as_deref(),
294
+
telegram_username: input.telegram_username.as_deref(),
295
+
signal_username: input.signal_username.as_deref(),
296
+
},
297
+
) {
298
+
Ok(r) => r,
299
+
Err(e) => return e.into_response(),
191
300
},
192
-
})
301
+
)
193
302
};
194
303
let hostname = &cfg.server.hostname;
195
-
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
196
-
if let Some(signing_key_did) = &input.signing_key {
197
-
match state
198
-
.infra_repo
199
-
.get_reserved_signing_key(signing_key_did)
200
-
.await
201
-
{
202
-
Ok(Some(key)) => (key.private_key_bytes, Some(key.id)),
203
-
Ok(None) => {
204
-
return ApiError::InvalidSigningKey.into_response();
205
-
}
206
-
Err(e) => {
207
-
error!("Error looking up reserved signing key: {:?}", e);
208
-
return ApiError::InternalError(None).into_response();
209
-
}
210
-
}
211
-
} else {
212
-
let secret_key = SecretKey::random(&mut OsRng);
213
-
(secret_key.to_bytes().to_vec(), None)
304
+
let key_result =
305
+
match super::provision::resolve_signing_key(&state, input.signing_key.as_deref()).await {
306
+
Ok(k) => k,
307
+
Err(e) => return e.into_response(),
214
308
};
215
-
let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
216
-
Ok(k) => k,
217
-
Err(e) => {
218
-
error!("Error creating signing key: {:?}", e);
219
-
return ApiError::InternalError(None).into_response();
220
-
}
221
-
};
309
+
let secret_key_bytes = key_result.secret_key_bytes;
310
+
let signing_key = key_result.signing_key;
311
+
let reserved_key_id = key_result.reserved_key_id;
222
312
let did_type = input.did_type.as_deref().unwrap_or("plc");
223
313
let did = match did_type {
224
314
"web" => {
225
-
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
226
-
return ApiError::SelfHostedDidWebDisabled.into_response();
227
-
}
228
-
let encoded_handle = handle.replace(':', "%3A");
229
-
let self_hosted_did = format!("did:web:{}", encoded_handle);
315
+
let self_hosted_did = match common::create_self_hosted_did_web(&handle) {
316
+
Ok(d) => d,
317
+
Err(e) => return e.into_response(),
318
+
};
230
319
info!(did = %self_hosted_did, "Creating self-hosted did:web account (subdomain)");
231
320
self_hosted_did
232
321
}
···
287
376
}
288
377
}
289
378
};
290
-
if is_migration {
291
-
let did_typed: Did = match did.parse() {
292
-
Ok(d) => d,
293
-
Err(_) => return ApiError::InternalError(Some("Invalid DID".into())).into_response(),
294
-
};
295
-
let handle_typed: Handle = match handle.parse() {
296
-
Ok(h) => h,
297
-
Err(_) => return ApiError::InvalidHandle(None).into_response(),
298
-
};
299
-
let reactivate_input = tranquil_db_traits::MigrationReactivationInput {
300
-
did: did_typed.clone(),
301
-
new_handle: handle_typed.clone(),
302
-
new_email: email.clone(),
303
-
};
304
-
match state
305
-
.user_repo
306
-
.reactivate_migration_account(&reactivate_input)
307
-
.await
308
-
{
309
-
Ok(reactivated) => {
310
-
info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration");
311
-
let secret_key_bytes = match state
312
-
.user_repo
313
-
.get_user_key_by_id(reactivated.user_id)
314
-
.await
315
-
{
316
-
Ok(Some(key_info)) => {
317
-
match tranquil_pds::config::decrypt_key(
318
-
&key_info.key_bytes,
319
-
key_info.encryption_version,
320
-
) {
321
-
Ok(k) => k,
322
-
Err(e) => {
323
-
error!("Error decrypting key for reactivated account: {:?}", e);
324
-
return ApiError::InternalError(None).into_response();
325
-
}
326
-
}
327
-
}
328
-
_ => {
329
-
error!("No signing key found for reactivated account");
330
-
return ApiError::InternalError(Some(
331
-
"Account signing key not found".into(),
332
-
))
333
-
.into_response();
334
-
}
335
-
};
336
-
let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(
337
-
&did,
338
-
&secret_key_bytes,
339
-
) {
340
-
Ok(m) => m,
341
-
Err(e) => {
342
-
error!("Error creating access token: {:?}", e);
343
-
return ApiError::InternalError(None).into_response();
344
-
}
345
-
};
346
-
let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(
347
-
&did,
348
-
&secret_key_bytes,
349
-
) {
350
-
Ok(m) => m,
351
-
Err(e) => {
352
-
error!("Error creating refresh token: {:?}", e);
353
-
return ApiError::InternalError(None).into_response();
354
-
}
355
-
};
356
-
let session_data = tranquil_db_traits::SessionTokenCreate {
357
-
did: did_typed.clone(),
358
-
access_jti: access_meta.jti.clone(),
359
-
refresh_jti: refresh_meta.jti.clone(),
360
-
access_expires_at: access_meta.expires_at,
361
-
refresh_expires_at: refresh_meta.expires_at,
362
-
login_type: tranquil_db_traits::LoginType::Modern,
363
-
mfa_verified: false,
364
-
scope: Some("transition:generic transition:chat.bsky".to_string()),
365
-
controller_did: None,
366
-
app_password_name: None,
367
-
};
368
-
if let Err(e) = state.session_repo.create_session(&session_data).await {
369
-
error!("Error creating session: {:?}", e);
370
-
return ApiError::InternalError(None).into_response();
371
-
}
372
-
let hostname = &tranquil_config::get().server.hostname;
373
-
let verification_required = if let Some(ref recipient) = verification_recipient {
374
-
let token = tranquil_pds::auth::verification_token::generate_migration_token(
375
-
&did_typed,
376
-
verification_channel,
377
-
recipient,
378
-
);
379
-
let formatted_token =
380
-
tranquil_pds::auth::verification_token::format_token_for_display(&token);
381
-
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
382
-
state.user_repo.as_ref(),
383
-
state.infra_repo.as_ref(),
384
-
reactivated.user_id,
385
-
verification_channel,
386
-
recipient,
387
-
&formatted_token,
388
-
hostname,
389
-
)
390
-
.await
391
-
{
392
-
warn!("Failed to enqueue migration verification: {:?}", e);
393
-
}
394
-
true
395
-
} else {
396
-
false
397
-
};
398
-
return (
399
-
axum::http::StatusCode::OK,
400
-
Json(CreateAccountOutput {
401
-
handle: handle.clone().into(),
402
-
did: did_typed.clone(),
403
-
did_doc: state.did_resolver.resolve_did_document(&did).await,
404
-
access_jwt: access_meta.token,
405
-
refresh_jwt: refresh_meta.token,
406
-
verification_required,
407
-
verification_channel,
408
-
}),
409
-
)
410
-
.into_response();
411
-
}
412
-
Err(tranquil_db_traits::MigrationReactivationError::NotFound) => {}
413
-
Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated) => {
414
-
return ApiError::AccountAlreadyExists.into_response();
415
-
}
416
-
Err(tranquil_db_traits::MigrationReactivationError::HandleTaken) => {
417
-
return ApiError::HandleTaken.into_response();
418
-
}
419
-
Err(e) => {
420
-
error!("Error reactivating migration account: {:?}", e);
421
-
return ApiError::InternalError(None).into_response();
422
-
}
423
-
}
379
+
if is_migration
380
+
&& let Some(response) = try_reactivate_migration(
381
+
&state,
382
+
&did,
383
+
&handle,
384
+
&email,
385
+
verification_channel,
386
+
verification_recipient.as_deref(),
387
+
)
388
+
.await
389
+
{
390
+
return response;
424
391
}
425
392
426
393
let handle_typed: Handle = match handle.parse() {
···
528
495
None
529
496
};
530
497
498
+
let comms = super::provision::normalize_comms_usernames(
499
+
input.discord_username.as_deref(),
500
+
input.telegram_username.as_deref(),
501
+
input.signal_username.as_deref(),
502
+
);
531
503
let preferred_comms_channel = verification_channel;
504
+
let repo_for_seq = repo.clone();
532
505
533
506
let create_input = tranquil_db_traits::CreatePasswordAccountInput {
534
507
handle: handle_typed.clone(),
···
536
509
did: did_for_commit.clone(),
537
510
password_hash,
538
511
preferred_comms_channel,
539
-
discord_username: input
540
-
.discord_username
541
-
.as_deref()
542
-
.map(|s| s.trim().to_lowercase())
543
-
.filter(|s| !s.is_empty()),
544
-
telegram_username: input
545
-
.telegram_username
546
-
.as_deref()
547
-
.map(|s| s.trim().trim_start_matches('@'))
548
-
.filter(|s| !s.is_empty())
549
-
.map(String::from),
550
-
signal_username: input
551
-
.signal_username
552
-
.as_deref()
553
-
.map(|s| s.trim().trim_start_matches('@'))
554
-
.filter(|s| !s.is_empty())
555
-
.map(|s| s.to_lowercase()),
512
+
discord_username: comms.discord,
513
+
telegram_username: comms.telegram,
514
+
signal_username: comms.signal,
556
515
deactivated_at,
557
516
encrypted_key_bytes: repo.encrypted_key_bytes,
558
517
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
···
586
545
};
587
546
let user_id = create_result.user_id;
588
547
if !is_migration && !is_did_web_byod {
589
-
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(
590
-
&state,
591
-
&did_for_commit,
592
-
Some(&handle_typed),
593
-
)
594
-
.await
595
-
{
596
-
warn!("Failed to sequence identity event for {}: {}", did, e);
597
-
}
598
-
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
599
-
&state,
600
-
&did_for_commit,
601
-
tranquil_db_traits::AccountStatus::Active,
602
-
)
603
-
.await
604
-
{
605
-
warn!("Failed to sequence account event for {}: {}", did, e);
606
-
}
607
-
if let Err(e) = tranquil_pds::repo_ops::sequence_genesis_commit(
548
+
super::provision::sequence_new_account(
608
549
&state,
609
550
&did_for_commit,
610
-
&repo.commit_cid,
611
-
&repo.mst_root_cid,
612
-
&rev_str,
551
+
&handle_typed,
552
+
&repo_for_seq,
553
+
&input.handle,
613
554
)
614
-
.await
615
-
{
616
-
warn!("Failed to sequence commit event for {}: {}", did, e);
617
-
}
618
-
if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event(
619
-
&state,
620
-
&did_for_commit,
621
-
&commit_cid_str,
622
-
Some(&rev_str),
623
-
)
624
-
.await
625
-
{
626
-
warn!("Failed to sequence sync event for {}: {}", did, e);
627
-
}
628
-
let profile_record = json!({
629
-
"$type": "app.bsky.actor.profile",
630
-
"displayName": input.handle
631
-
});
632
-
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
633
-
&state,
634
-
&did_for_commit,
635
-
&tranquil_pds::types::PROFILE_COLLECTION,
636
-
&tranquil_pds::types::PROFILE_RKEY,
637
-
&profile_record,
638
-
)
639
-
.await
640
-
{
641
-
warn!("Failed to create default profile for {}: {}", did, e);
642
-
}
555
+
.await;
643
556
}
644
-
let hostname = &tranquil_config::get().server.hostname;
645
557
if !is_migration {
646
558
if let Some(ref recipient) = verification_recipient {
647
-
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
648
-
&did_for_commit,
649
-
verification_channel,
650
-
recipient,
651
-
);
652
-
let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(
653
-
&verification_token,
654
-
);
655
-
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
656
-
state.user_repo.as_ref(),
657
-
state.infra_repo.as_ref(),
559
+
super::provision::enqueue_signup_verification(
560
+
&state,
658
561
user_id,
562
+
&did_for_commit,
659
563
verification_channel,
660
564
recipient,
661
-
&formatted_token,
662
-
hostname,
663
565
)
664
-
.await
665
-
{
666
-
warn!(
667
-
"Failed to enqueue signup verification notification: {:?}",
668
-
e
669
-
);
670
-
}
566
+
.await;
671
567
}
672
568
} else if let Some(ref recipient) = verification_recipient {
673
-
let token = tranquil_pds::auth::verification_token::generate_migration_token(
674
-
&did_for_commit,
675
-
verification_channel,
676
-
recipient,
677
-
);
678
-
let formatted_token =
679
-
tranquil_pds::auth::verification_token::format_token_for_display(&token);
680
-
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
681
-
state.user_repo.as_ref(),
682
-
state.infra_repo.as_ref(),
569
+
super::provision::enqueue_migration_verification(
570
+
&state,
683
571
user_id,
572
+
&did_for_commit,
684
573
verification_channel,
685
574
recipient,
686
-
&formatted_token,
687
-
hostname,
688
575
)
689
-
.await
690
-
{
691
-
warn!("Failed to enqueue migration verification: {:?}", e);
692
-
}
576
+
.await;
693
577
}
694
578
695
-
let access_meta =
696
-
match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
697
-
Ok(m) => m,
698
-
Err(e) => {
699
-
error!("createAccount: Error creating access token: {:?}", e);
700
-
return ApiError::InternalError(None).into_response();
701
-
}
702
-
};
703
-
let refresh_meta =
704
-
match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) {
705
-
Ok(m) => m,
706
-
Err(e) => {
707
-
error!("createAccount: Error creating refresh token: {:?}", e);
708
-
return ApiError::InternalError(None).into_response();
709
-
}
710
-
};
711
-
let session_data = tranquil_db_traits::SessionTokenCreate {
712
-
did: did_for_commit.clone(),
713
-
access_jti: access_meta.jti.clone(),
714
-
refresh_jti: refresh_meta.jti.clone(),
715
-
access_expires_at: access_meta.expires_at,
716
-
refresh_expires_at: refresh_meta.expires_at,
717
-
login_type: tranquil_db_traits::LoginType::Modern,
718
-
mfa_verified: false,
719
-
scope: Some("transition:generic transition:chat.bsky".to_string()),
720
-
controller_did: None,
721
-
app_password_name: None,
579
+
let session = match super::provision::create_and_store_session(
580
+
&state,
581
+
&did,
582
+
&did_for_commit,
583
+
&secret_key_bytes,
584
+
"transition:generic transition:chat.bsky",
585
+
None,
586
+
)
587
+
.await
588
+
{
589
+
Ok(s) => s,
590
+
Err(e) => return e.into_response(),
722
591
};
723
-
if let Err(e) = state.session_repo.create_session(&session_data).await {
724
-
error!("createAccount: Error creating session: {:?}", e);
725
-
return ApiError::InternalError(None).into_response();
726
-
}
727
592
728
593
let did_doc = state.did_resolver.resolve_did_document(&did).await;
729
594
···
740
605
handle: handle.clone().into(),
741
606
did: did_for_commit,
742
607
did_doc,
743
-
access_jwt: access_meta.token,
744
-
refresh_jwt: refresh_meta.token,
608
+
access_jwt: session.access_jwt,
609
+
refresh_jwt: session.refresh_jwt,
745
610
verification_required: !is_migration,
746
611
verification_channel,
747
612
}),
+5
-10
crates/tranquil-api/src/identity/plc/request.rs
+5
-10
crates/tranquil-api/src/identity/plc/request.rs
···
1
-
use axum::{
2
-
extract::State,
3
-
response::{IntoResponse, Response},
4
-
};
1
+
use axum::{Json, extract::State};
5
2
use chrono::{Duration, Utc};
6
3
use tracing::{info, warn};
7
4
use tranquil_pds::api::EmptyResponse;
···
16
13
pub async fn request_plc_operation_signature(
17
14
State(state): State<AppState>,
18
15
auth: Auth<Permissive>,
19
-
) -> Result<Response, ApiError> {
20
-
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
16
+
) -> Result<Json<EmptyResponse>, ApiError> {
17
+
tranquil_pds::auth::scope_check::check_identity_scope(
21
18
&auth.auth_source,
22
19
auth.scope.as_deref(),
23
20
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
24
-
) {
25
-
return Ok(e);
26
-
}
21
+
)?;
27
22
let user_id = state
28
23
.user_repo
29
24
.get_id_by_did(&auth.did)
···
53
48
warn!("Failed to enqueue PLC operation notification: {:?}", e);
54
49
}
55
50
info!("PLC operation signature requested for user {}", auth.did);
56
-
Ok(EmptyResponse::ok().into_response())
51
+
Ok(Json(EmptyResponse {}))
57
52
}
+7
-18
crates/tranquil-api/src/identity/plc/sign.rs
+7
-18
crates/tranquil-api/src/identity/plc/sign.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 chrono::Utc;
8
3
use k256::ecdsa::SigningKey;
9
4
use serde::{Deserialize, Serialize};
···
43
38
State(state): State<AppState>,
44
39
auth: Auth<Permissive>,
45
40
Json(input): Json<SignPlcOperationInput>,
46
-
) -> Result<Response, ApiError> {
47
-
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
41
+
) -> Result<Json<SignPlcOperationOutput>, ApiError> {
42
+
tranquil_pds::auth::scope_check::check_identity_scope(
48
43
&auth.auth_source,
49
44
auth.scope.as_deref(),
50
45
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
51
-
) {
52
-
return Ok(e);
53
-
}
46
+
)?;
54
47
let did = &auth.did;
55
48
if did.starts_with("did:web:") {
56
49
return Err(ApiError::InvalidRequest(
···
145
138
146
139
let _ = state.infra_repo.delete_plc_token(user_id, token).await;
147
140
info!("Signed PLC operation for user {}", did);
148
-
Ok((
149
-
StatusCode::OK,
150
-
Json(SignPlcOperationOutput {
151
-
operation: signed_op,
152
-
}),
153
-
)
154
-
.into_response())
141
+
Ok(Json(SignPlcOperationOutput {
142
+
operation: signed_op,
143
+
}))
155
144
}
+14
-20
crates/tranquil-api/src/identity/plc/submit.rs
+14
-20
crates/tranquil-api/src/identity/plc/submit.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
response::{IntoResponse, Response},
5
-
};
1
+
use axum::{Json, extract::State};
6
2
use k256::ecdsa::SigningKey;
7
3
use serde::Deserialize;
8
4
use serde_json::Value;
···
23
19
State(state): State<AppState>,
24
20
auth: Auth<Permissive>,
25
21
Json(input): Json<SubmitPlcOperationInput>,
26
-
) -> Result<Response, ApiError> {
27
-
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
22
+
) -> Result<Json<EmptyResponse>, ApiError> {
23
+
tranquil_pds::auth::scope_check::check_identity_scope(
28
24
&auth.auth_source,
29
25
auth.scope.as_deref(),
30
26
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
31
-
) {
32
-
return Ok(e);
33
-
}
27
+
)?;
34
28
let did = &auth.did;
35
29
if did.starts_with("did:web:") {
36
30
return Err(ApiError::InvalidRequest(
···
76
70
.plc_rotation_key
77
71
.clone()
78
72
.unwrap_or_else(|| user_did_key.clone());
79
-
if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) {
73
+
if let Some(rotation_keys) = op.get("rotationKeys").and_then(Value::as_array) {
80
74
let has_server_key = rotation_keys
81
75
.iter()
82
76
.any(|k| k.as_str() == Some(&server_rotation_key));
···
86
80
));
87
81
}
88
82
}
89
-
if let Some(services) = op.get("services").and_then(|v| v.as_object())
90
-
&& let Some(pds) = services.get("atproto_pds").and_then(|v| v.as_object())
83
+
if let Some(services) = op.get("services").and_then(Value::as_object)
84
+
&& let Some(pds) = services.get("atproto_pds").and_then(Value::as_object)
91
85
{
92
-
let service_type = pds.get("type").and_then(|v| v.as_str());
93
-
let endpoint = pds.get("endpoint").and_then(|v| v.as_str());
86
+
let service_type = pds.get("type").and_then(Value::as_str);
87
+
let endpoint = pds.get("endpoint").and_then(Value::as_str);
94
88
if service_type != Some(tranquil_pds::plc::ServiceType::Pds.as_str()) {
95
89
return Err(ApiError::InvalidRequest(
96
90
"Incorrect type on atproto_pds service".into(),
···
102
96
));
103
97
}
104
98
}
105
-
if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object())
106
-
&& let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str())
99
+
if let Some(verification_methods) = op.get("verificationMethods").and_then(Value::as_object)
100
+
&& let Some(atproto_key) = verification_methods.get("atproto").and_then(Value::as_str)
107
101
&& atproto_key != user_did_key
108
102
{
109
103
return Err(ApiError::InvalidRequest(
···
111
105
));
112
106
}
113
107
if let Some(also_known_as) = (!user.handle.is_empty())
114
-
.then(|| op.get("alsoKnownAs").and_then(|v| v.as_array()))
108
+
.then(|| op.get("alsoKnownAs").and_then(Value::as_array))
115
109
.flatten()
116
110
{
117
111
let expected_handle = format!("at://{}", user.handle);
118
-
let first_aka = also_known_as.first().and_then(|v| v.as_str());
112
+
let first_aka = also_known_as.first().and_then(Value::as_str);
119
113
if first_aka != Some(&expected_handle) {
120
114
return Err(ApiError::InvalidRequest(
121
115
"Incorrect handle in alsoKnownAs".into(),
···
163
157
warn!(did = %did, "Failed to refresh DID cache after PLC update");
164
158
}
165
159
info!(did = %did, "PLC operation submitted successfully");
166
-
Ok(EmptyResponse::ok().into_response())
160
+
Ok(Json(EmptyResponse {}))
167
161
}
+240
-1
crates/tranquil-api/src/identity/provision.rs
+240
-1
crates/tranquil-api/src/identity/provision.rs
···
2
2
use jacquard_repo::{mst::Mst, storage::BlockStore};
3
3
use k256::ecdsa::SigningKey;
4
4
use std::sync::Arc;
5
+
use tranquil_db_traits::CommsChannel;
5
6
use tranquil_pds::api::error::ApiError;
6
7
use tranquil_pds::repo_ops::create_signed_commit;
7
8
use tranquil_pds::state::AppState;
8
-
use tranquil_pds::types::Did;
9
+
use tranquil_pds::types::{Did, Handle};
9
10
10
11
pub struct PlcDidResult {
11
12
pub did: Did,
···
74
75
Ok(genesis_result.did)
75
76
}
76
77
78
+
#[derive(Clone)]
77
79
pub struct GenesisRepo {
78
80
pub encrypted_key_bytes: Vec<u8>,
79
81
pub commit_cid: cid::Cid,
···
120
122
genesis_block_cids: vec![mst_root.to_bytes(), commit_cid.to_bytes()],
121
123
})
122
124
}
125
+
126
+
pub struct SigningKeyResult {
127
+
pub secret_key_bytes: Vec<u8>,
128
+
pub signing_key: SigningKey,
129
+
pub reserved_key_id: Option<uuid::Uuid>,
130
+
}
131
+
132
+
pub async fn resolve_signing_key(
133
+
state: &AppState,
134
+
signing_key_did: Option<&str>,
135
+
) -> Result<SigningKeyResult, ApiError> {
136
+
match signing_key_did {
137
+
Some(key_did) => {
138
+
let key = state
139
+
.infra_repo
140
+
.get_reserved_signing_key(key_did)
141
+
.await
142
+
.map_err(|e| {
143
+
tracing::error!("Error looking up reserved signing key: {:?}", e);
144
+
ApiError::InternalError(None)
145
+
})?
146
+
.ok_or(ApiError::InvalidSigningKey)?;
147
+
let signing_key = SigningKey::from_slice(&key.private_key_bytes).map_err(|e| {
148
+
tracing::error!("Error creating signing key: {:?}", e);
149
+
ApiError::InternalError(None)
150
+
})?;
151
+
Ok(SigningKeyResult {
152
+
secret_key_bytes: key.private_key_bytes,
153
+
signing_key,
154
+
reserved_key_id: Some(key.id),
155
+
})
156
+
}
157
+
None => {
158
+
use k256::SecretKey;
159
+
use rand::rngs::OsRng;
160
+
let secret_key = SecretKey::random(&mut OsRng);
161
+
let secret_key_bytes = secret_key.to_bytes().to_vec();
162
+
let signing_key = SigningKey::from_slice(&secret_key_bytes).map_err(|e| {
163
+
tracing::error!("Error creating signing key: {:?}", e);
164
+
ApiError::InternalError(None)
165
+
})?;
166
+
Ok(SigningKeyResult {
167
+
secret_key_bytes,
168
+
signing_key,
169
+
reserved_key_id: None,
170
+
})
171
+
}
172
+
}
173
+
}
174
+
175
+
pub async fn sequence_new_account(
176
+
state: &AppState,
177
+
did: &Did,
178
+
handle: &Handle,
179
+
repo: &GenesisRepo,
180
+
display_name: &str,
181
+
) {
182
+
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(state, did, Some(handle)).await
183
+
{
184
+
tracing::warn!("Failed to sequence identity event for {}: {}", did, e);
185
+
}
186
+
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
187
+
state,
188
+
did,
189
+
tranquil_db_traits::AccountStatus::Active,
190
+
)
191
+
.await
192
+
{
193
+
tracing::warn!("Failed to sequence account event for {}: {}", did, e);
194
+
}
195
+
if let Err(e) = tranquil_pds::repo_ops::sequence_genesis_commit(
196
+
state,
197
+
did,
198
+
&repo.commit_cid,
199
+
&repo.mst_root_cid,
200
+
&repo.repo_rev,
201
+
)
202
+
.await
203
+
{
204
+
tracing::warn!("Failed to sequence commit event for {}: {}", did, e);
205
+
}
206
+
if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event(
207
+
state,
208
+
did,
209
+
&repo.commit_cid.to_string(),
210
+
Some(&repo.repo_rev),
211
+
)
212
+
.await
213
+
{
214
+
tracing::warn!("Failed to sequence sync event for {}: {}", did, e);
215
+
}
216
+
let profile_record = serde_json::json!({
217
+
"$type": "app.bsky.actor.profile",
218
+
"displayName": display_name
219
+
});
220
+
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
221
+
state,
222
+
did,
223
+
&tranquil_pds::types::PROFILE_COLLECTION,
224
+
&tranquil_pds::types::PROFILE_RKEY,
225
+
&profile_record,
226
+
)
227
+
.await
228
+
{
229
+
tracing::warn!("Failed to create default profile for {}: {}", did, e);
230
+
}
231
+
}
232
+
233
+
pub struct CommsUsernames {
234
+
pub discord: Option<String>,
235
+
pub telegram: Option<String>,
236
+
pub signal: Option<String>,
237
+
}
238
+
239
+
pub fn normalize_comms_usernames(
240
+
discord: Option<&str>,
241
+
telegram: Option<&str>,
242
+
signal: Option<&str>,
243
+
) -> CommsUsernames {
244
+
CommsUsernames {
245
+
discord: discord
246
+
.map(|s| s.trim().to_lowercase())
247
+
.filter(|s| !s.is_empty()),
248
+
telegram: telegram
249
+
.map(|s| s.trim().trim_start_matches('@'))
250
+
.filter(|s| !s.is_empty())
251
+
.map(String::from),
252
+
signal: signal
253
+
.map(|s| s.trim().trim_start_matches('@'))
254
+
.filter(|s| !s.is_empty())
255
+
.map(|s| s.to_lowercase()),
256
+
}
257
+
}
258
+
259
+
pub struct SessionResult {
260
+
pub access_jwt: String,
261
+
pub refresh_jwt: String,
262
+
}
263
+
264
+
pub async fn create_and_store_session(
265
+
state: &AppState,
266
+
did_str: &str,
267
+
did: &Did,
268
+
signing_key_bytes: &[u8],
269
+
scope: &str,
270
+
controller_did: Option<&Did>,
271
+
) -> Result<SessionResult, ApiError> {
272
+
let access_meta =
273
+
tranquil_pds::auth::create_access_token_with_metadata(did_str, signing_key_bytes).map_err(
274
+
|e| {
275
+
tracing::error!("Error creating access token: {:?}", e);
276
+
ApiError::InternalError(None)
277
+
},
278
+
)?;
279
+
let refresh_meta =
280
+
tranquil_pds::auth::create_refresh_token_with_metadata(did_str, signing_key_bytes)
281
+
.map_err(|e| {
282
+
tracing::error!("Error creating refresh token: {:?}", e);
283
+
ApiError::InternalError(None)
284
+
})?;
285
+
let session_data = tranquil_db_traits::SessionTokenCreate {
286
+
did: did.clone(),
287
+
access_jti: access_meta.jti.clone(),
288
+
refresh_jti: refresh_meta.jti.clone(),
289
+
access_expires_at: access_meta.expires_at,
290
+
refresh_expires_at: refresh_meta.expires_at,
291
+
login_type: tranquil_db_traits::LoginType::Modern,
292
+
mfa_verified: false,
293
+
scope: Some(scope.to_string()),
294
+
controller_did: controller_did.cloned(),
295
+
app_password_name: None,
296
+
};
297
+
state
298
+
.session_repo
299
+
.create_session(&session_data)
300
+
.await
301
+
.map_err(|e| {
302
+
tracing::error!("Error creating session: {:?}", e);
303
+
ApiError::InternalError(None)
304
+
})?;
305
+
Ok(SessionResult {
306
+
access_jwt: access_meta.token,
307
+
refresh_jwt: refresh_meta.token,
308
+
})
309
+
}
310
+
311
+
pub async fn enqueue_signup_verification(
312
+
state: &AppState,
313
+
user_id: uuid::Uuid,
314
+
did: &Did,
315
+
channel: CommsChannel,
316
+
recipient: &str,
317
+
) {
318
+
let token =
319
+
tranquil_pds::auth::verification_token::generate_signup_token(did, channel, recipient);
320
+
let formatted = tranquil_pds::auth::verification_token::format_token_for_display(&token);
321
+
let hostname = &tranquil_config::get().server.hostname;
322
+
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
323
+
state.user_repo.as_ref(),
324
+
state.infra_repo.as_ref(),
325
+
user_id,
326
+
channel,
327
+
recipient,
328
+
&formatted,
329
+
hostname,
330
+
)
331
+
.await
332
+
{
333
+
tracing::warn!("Failed to enqueue signup verification: {:?}", e);
334
+
}
335
+
}
336
+
337
+
pub async fn enqueue_migration_verification(
338
+
state: &AppState,
339
+
user_id: uuid::Uuid,
340
+
did: &Did,
341
+
channel: CommsChannel,
342
+
recipient: &str,
343
+
) {
344
+
let token =
345
+
tranquil_pds::auth::verification_token::generate_migration_token(did, channel, recipient);
346
+
let formatted = tranquil_pds::auth::verification_token::format_token_for_display(&token);
347
+
let hostname = &tranquil_config::get().server.hostname;
348
+
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
349
+
state.user_repo.as_ref(),
350
+
state.infra_repo.as_ref(),
351
+
user_id,
352
+
channel,
353
+
recipient,
354
+
&formatted,
355
+
hostname,
356
+
)
357
+
.await
358
+
{
359
+
tracing::warn!("Failed to enqueue migration verification: {:?}", e);
360
+
}
361
+
}
History
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
refactor(api): extract account provisioning helpers, simplify create_account flow
expand 0 comments
pull request successfully merged