Our Personal Data Server from scratch! tranquil.farm
atproto pds rust postgresql fun oauth

refactor(api): extract account provisioning helpers, simplify create_account flow #81

merged opened by oyster.cafe targeting main from refactor/api
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mhi3qdcvvf22
+472 -390
Diff #0
+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
··· 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
··· 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
··· 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
··· 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
sign up or login to add to the discussion
oyster.cafe submitted #0
1 commit
expand
refactor(api): extract account provisioning helpers, simplify create_account flow
expand 0 comments
pull request successfully merged