Our Personal Data Server from scratch!
0

Configure Feed

Select the types of activity you want to include in your feed.

1use super::did::verify_did_web; 2use crate::api::error::ApiError; 3use crate::api::repo::record::utils::create_signed_commit; 4use crate::auth::{ServiceTokenVerifier, is_service_token}; 5use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key}; 6use crate::state::{AppState, RateLimitKind}; 7use crate::types::{Did, Handle, Nsid, PlainPassword, Rkey}; 8use crate::validation::validate_password; 9use axum::{ 10 Json, 11 extract::State, 12 http::{HeaderMap, StatusCode}, 13 response::{IntoResponse, Response}, 14}; 15use bcrypt::{DEFAULT_COST, hash}; 16use jacquard_common::types::{integer::LimitedU32, string::Tid}; 17use jacquard_repo::{mst::Mst, storage::BlockStore}; 18use k256::{SecretKey, ecdsa::SigningKey}; 19use rand::rngs::OsRng; 20use serde::{Deserialize, Serialize}; 21use serde_json::json; 22use std::sync::Arc; 23use tracing::{debug, error, info, warn}; 24 25fn extract_client_ip(headers: &HeaderMap) -> String { 26 if let Some(forwarded) = headers.get("x-forwarded-for") 27 && let Ok(value) = forwarded.to_str() 28 && let Some(first_ip) = value.split(',').next() 29 { 30 return first_ip.trim().to_string(); 31 } 32 if let Some(real_ip) = headers.get("x-real-ip") 33 && let Ok(value) = real_ip.to_str() 34 { 35 return value.trim().to_string(); 36 } 37 "unknown".to_string() 38} 39 40#[derive(Deserialize)] 41#[serde(rename_all = "camelCase")] 42pub struct CreateAccountInput { 43 pub handle: String, 44 pub email: Option<String>, 45 pub password: PlainPassword, 46 pub invite_code: Option<String>, 47 pub did: Option<String>, 48 pub did_type: Option<String>, 49 pub signing_key: Option<String>, 50 pub verification_channel: Option<String>, 51 pub discord_id: Option<String>, 52 pub telegram_username: Option<String>, 53 pub signal_number: Option<String>, 54} 55 56#[derive(Serialize)] 57#[serde(rename_all = "camelCase")] 58pub struct CreateAccountOutput { 59 pub handle: Handle, 60 pub did: Did, 61 #[serde(skip_serializing_if = "Option::is_none")] 62 pub did_doc: Option<serde_json::Value>, 63 pub access_jwt: String, 64 pub refresh_jwt: String, 65 pub verification_required: bool, 66 pub verification_channel: String, 67} 68 69pub async fn create_account( 70 State(state): State<AppState>, 71 headers: HeaderMap, 72 Json(input): Json<CreateAccountInput>, 73) -> Response { 74 let is_potential_migration = input 75 .did 76 .as_ref() 77 .map(|d| d.starts_with("did:plc:")) 78 .unwrap_or(false); 79 if is_potential_migration { 80 info!( 81 "[MIGRATION] createAccount called for potential migration did={:?} handle={}", 82 input.did, input.handle 83 ); 84 } else { 85 info!("create_account called"); 86 } 87 let client_ip = extract_client_ip(&headers); 88 if !state 89 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 90 .await 91 { 92 warn!(ip = %client_ip, "Account creation rate limit exceeded"); 93 return ApiError::RateLimitExceeded(Some( 94 "Too many account creation attempts. Please try again later.".into(), 95 )) 96 .into_response(); 97 } 98 99 let migration_auth = if let Some(extracted) = crate::auth::extract_auth_token_from_header( 100 headers.get("Authorization").and_then(|h| h.to_str().ok()), 101 ) { 102 let token = extracted.token; 103 if is_service_token(&token) { 104 let verifier = ServiceTokenVerifier::new(); 105 match verifier 106 .verify_service_token(&token, Some("com.atproto.server.createAccount")) 107 .await 108 { 109 Ok(claims) => { 110 debug!("Service token verified for migration: iss={}", claims.iss); 111 Some(claims.iss) 112 } 113 Err(e) => { 114 error!("Service token verification failed: {:?}", e); 115 return ApiError::AuthenticationFailed(Some(format!( 116 "Service token verification failed: {}", 117 e 118 ))) 119 .into_response(); 120 } 121 } 122 } else { 123 None 124 } 125 } else { 126 None 127 }; 128 129 let is_did_web_byod = migration_auth.is_some() 130 && input 131 .did 132 .as_ref() 133 .map(|d| d.starts_with("did:web:")) 134 .unwrap_or(false); 135 136 let is_migration = migration_auth.is_some() 137 && input 138 .did 139 .as_ref() 140 .map(|d| d.starts_with("did:plc:")) 141 .unwrap_or(false); 142 143 if (is_migration || is_did_web_byod) 144 && let (Some(provided_did), Some(auth_did)) = (input.did.as_ref(), migration_auth.as_ref()) 145 { 146 if provided_did != auth_did { 147 info!( 148 "[MIGRATION] createAccount: Service token mismatch - token_did={} provided_did={}", 149 auth_did, provided_did 150 ); 151 return ApiError::AuthorizationError(format!( 152 "Service token issuer {} does not match DID {}", 153 auth_did, provided_did 154 )) 155 .into_response(); 156 } 157 if is_did_web_byod { 158 info!(did = %provided_did, "Processing did:web BYOD account creation"); 159 } else { 160 info!( 161 "[MIGRATION] createAccount: Service token verified, processing migration for did={}", 162 provided_did 163 ); 164 } 165 } 166 167 let hostname_for_validation = 168 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 169 let pds_suffix = format!(".{}", hostname_for_validation); 170 171 let validated_short_handle = if !input.handle.contains('.') 172 || input.handle.ends_with(&pds_suffix) 173 { 174 let handle_to_validate = if input.handle.ends_with(&pds_suffix) { 175 input 176 .handle 177 .strip_suffix(&pds_suffix) 178 .unwrap_or(&input.handle) 179 } else { 180 &input.handle 181 }; 182 match crate::api::validation::validate_short_handle(handle_to_validate) { 183 Ok(h) => h, 184 Err(e) => { 185 return ApiError::from(e).into_response(); 186 } 187 } 188 } else { 189 if input.handle.contains(' ') || input.handle.contains('\t') { 190 return ApiError::InvalidRequest("Handle cannot contain spaces".into()).into_response(); 191 } 192 if let Some(c) = input 193 .handle 194 .chars() 195 .find(|c| !c.is_ascii_alphanumeric() && *c != '.' && *c != '-') 196 { 197 return ApiError::InvalidRequest(format!("Handle contains invalid character: {}", c)) 198 .into_response(); 199 } 200 let handle_lower = input.handle.to_lowercase(); 201 if crate::moderation::has_explicit_slur(&handle_lower) { 202 return ApiError::InvalidRequest("Inappropriate language in handle".into()) 203 .into_response(); 204 } 205 handle_lower 206 }; 207 let email: Option<String> = input 208 .email 209 .as_ref() 210 .map(|e| e.trim().to_string()) 211 .filter(|e| !e.is_empty()); 212 if let Some(ref email) = email 213 && !crate::api::validation::is_valid_email(email) 214 { 215 return ApiError::InvalidEmail.into_response(); 216 } 217 let verification_channel = input.verification_channel.as_deref().unwrap_or("email"); 218 let valid_channels = ["email", "discord", "telegram", "signal"]; 219 if !valid_channels.contains(&verification_channel) && !is_migration { 220 return ApiError::InvalidVerificationChannel.into_response(); 221 } 222 let verification_recipient = if is_migration { 223 None 224 } else { 225 Some(match verification_channel { 226 "email" => match &input.email { 227 Some(email) if !email.trim().is_empty() => email.trim().to_string(), 228 _ => return ApiError::MissingEmail.into_response(), 229 }, 230 "discord" => match &input.discord_id { 231 Some(id) if !id.trim().is_empty() => id.trim().to_string(), 232 _ => return ApiError::MissingDiscordId.into_response(), 233 }, 234 "telegram" => match &input.telegram_username { 235 Some(username) if !username.trim().is_empty() => username.trim().to_string(), 236 _ => return ApiError::MissingTelegramUsername.into_response(), 237 }, 238 "signal" => match &input.signal_number { 239 Some(number) if !number.trim().is_empty() => number.trim().to_string(), 240 _ => return ApiError::MissingSignalNumber.into_response(), 241 }, 242 _ => return ApiError::InvalidVerificationChannel.into_response(), 243 }) 244 }; 245 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 246 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 247 let pds_endpoint = format!("https://{}", hostname); 248 let suffix = format!(".{}", hostname_for_handles); 249 let handle = if input.handle.ends_with(&suffix) { 250 format!("{}.{}", validated_short_handle, hostname_for_handles) 251 } else if input.handle.contains('.') { 252 validated_short_handle.clone() 253 } else { 254 format!("{}.{}", validated_short_handle, hostname_for_handles) 255 }; 256 let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) = 257 if let Some(signing_key_did) = &input.signing_key { 258 match state 259 .infra_repo 260 .get_reserved_signing_key(signing_key_did) 261 .await 262 { 263 Ok(Some(key)) => (key.private_key_bytes, Some(key.id)), 264 Ok(None) => { 265 return ApiError::InvalidSigningKey.into_response(); 266 } 267 Err(e) => { 268 error!("Error looking up reserved signing key: {:?}", e); 269 return ApiError::InternalError(None).into_response(); 270 } 271 } 272 } else { 273 let secret_key = SecretKey::random(&mut OsRng); 274 (secret_key.to_bytes().to_vec(), None) 275 }; 276 let signing_key = match SigningKey::from_slice(&secret_key_bytes) { 277 Ok(k) => k, 278 Err(e) => { 279 error!("Error creating signing key: {:?}", e); 280 return ApiError::InternalError(None).into_response(); 281 } 282 }; 283 let did_type = input.did_type.as_deref().unwrap_or("plc"); 284 let did = match did_type { 285 "web" => { 286 if !crate::api::server::meta::is_self_hosted_did_web_enabled() { 287 return ApiError::SelfHostedDidWebDisabled.into_response(); 288 } 289 let subdomain_host = format!("{}.{}", input.handle, hostname_for_handles); 290 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 291 let self_hosted_did = format!("did:web:{}", encoded_subdomain); 292 info!(did = %self_hosted_did, "Creating self-hosted did:web account (subdomain)"); 293 self_hosted_did 294 } 295 "web-external" => { 296 let d = match &input.did { 297 Some(d) if !d.trim().is_empty() => d, 298 _ => { 299 return ApiError::InvalidRequest( 300 "External did:web requires the 'did' field to be provided".into(), 301 ) 302 .into_response(); 303 } 304 }; 305 if !d.starts_with("did:web:") { 306 return ApiError::InvalidDid("External DID must be a did:web".into()) 307 .into_response(); 308 } 309 if !is_did_web_byod 310 && let Err(e) = 311 verify_did_web(d, &hostname, &input.handle, input.signing_key.as_deref()).await 312 { 313 return ApiError::InvalidDid(e).into_response(); 314 } 315 info!(did = %d, "Creating external did:web account"); 316 d.clone() 317 } 318 _ => { 319 if let Some(d) = &input.did { 320 if d.starts_with("did:plc:") && is_migration { 321 info!(did = %d, "Migration with existing did:plc"); 322 d.clone() 323 } else if d.starts_with("did:web:") { 324 if !is_did_web_byod 325 && let Err(e) = verify_did_web( 326 d, 327 &hostname, 328 &input.handle, 329 input.signing_key.as_deref(), 330 ) 331 .await 332 { 333 return ApiError::InvalidDid(e).into_response(); 334 } 335 d.clone() 336 } else if !d.trim().is_empty() { 337 return ApiError::InvalidDid( 338 "Only did:web DIDs can be provided; leave empty for did:plc. For migration with existing did:plc, provide service auth.".into() 339 ) 340 .into_response(); 341 } else { 342 let rotation_key = std::env::var("PLC_ROTATION_KEY") 343 .unwrap_or_else(|_| signing_key_to_did_key(&signing_key)); 344 let genesis_result = match create_genesis_operation( 345 &signing_key, 346 &rotation_key, 347 &handle, 348 &pds_endpoint, 349 ) { 350 Ok(r) => r, 351 Err(e) => { 352 error!("Error creating PLC genesis operation: {:?}", e); 353 return ApiError::InternalError(Some( 354 "Failed to create PLC operation".into(), 355 )) 356 .into_response(); 357 } 358 }; 359 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 360 if let Err(e) = plc_client 361 .send_operation(&genesis_result.did, &genesis_result.signed_operation) 362 .await 363 { 364 error!("Failed to submit PLC genesis operation: {:?}", e); 365 return ApiError::UpstreamErrorMsg(format!( 366 "Failed to register DID with PLC directory: {}", 367 e 368 )) 369 .into_response(); 370 } 371 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 372 genesis_result.did 373 } 374 } else { 375 let rotation_key = std::env::var("PLC_ROTATION_KEY") 376 .unwrap_or_else(|_| signing_key_to_did_key(&signing_key)); 377 let genesis_result = match create_genesis_operation( 378 &signing_key, 379 &rotation_key, 380 &handle, 381 &pds_endpoint, 382 ) { 383 Ok(r) => r, 384 Err(e) => { 385 error!("Error creating PLC genesis operation: {:?}", e); 386 return ApiError::InternalError(Some( 387 "Failed to create PLC operation".into(), 388 )) 389 .into_response(); 390 } 391 }; 392 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 393 if let Err(e) = plc_client 394 .send_operation(&genesis_result.did, &genesis_result.signed_operation) 395 .await 396 { 397 error!("Failed to submit PLC genesis operation: {:?}", e); 398 return ApiError::UpstreamErrorMsg(format!( 399 "Failed to register DID with PLC directory: {}", 400 e 401 )) 402 .into_response(); 403 } 404 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 405 genesis_result.did 406 } 407 } 408 }; 409 if is_migration { 410 let reactivate_input = tranquil_db_traits::MigrationReactivationInput { 411 did: Did::new_unchecked(&did), 412 new_handle: Handle::new_unchecked(&handle), 413 }; 414 match state 415 .user_repo 416 .reactivate_migration_account(&reactivate_input) 417 .await 418 { 419 Ok(reactivated) => { 420 info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration"); 421 let secret_key_bytes = match state 422 .user_repo 423 .get_user_key_by_id(reactivated.user_id) 424 .await 425 { 426 Ok(Some(key_info)) => { 427 match crate::config::decrypt_key( 428 &key_info.key_bytes, 429 key_info.encryption_version, 430 ) { 431 Ok(k) => k, 432 Err(e) => { 433 error!("Error decrypting key for reactivated account: {:?}", e); 434 return ApiError::InternalError(None).into_response(); 435 } 436 } 437 } 438 _ => { 439 error!("No signing key found for reactivated account"); 440 return ApiError::InternalError(Some( 441 "Account signing key not found".into(), 442 )) 443 .into_response(); 444 } 445 }; 446 let access_meta = 447 match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) { 448 Ok(m) => m, 449 Err(e) => { 450 error!("Error creating access token: {:?}", e); 451 return ApiError::InternalError(None).into_response(); 452 } 453 }; 454 let refresh_meta = match crate::auth::create_refresh_token_with_metadata( 455 &did, 456 &secret_key_bytes, 457 ) { 458 Ok(m) => m, 459 Err(e) => { 460 error!("Error creating refresh token: {:?}", e); 461 return ApiError::InternalError(None).into_response(); 462 } 463 }; 464 let session_data = tranquil_db_traits::SessionTokenCreate { 465 did: Did::new_unchecked(&did), 466 access_jti: access_meta.jti.clone(), 467 refresh_jti: refresh_meta.jti.clone(), 468 access_expires_at: access_meta.expires_at, 469 refresh_expires_at: refresh_meta.expires_at, 470 legacy_login: false, 471 mfa_verified: false, 472 scope: None, 473 controller_did: None, 474 app_password_name: None, 475 }; 476 if let Err(e) = state.session_repo.create_session(&session_data).await { 477 error!("Error creating session: {:?}", e); 478 return ApiError::InternalError(None).into_response(); 479 } 480 return ( 481 axum::http::StatusCode::OK, 482 Json(CreateAccountOutput { 483 handle: handle.clone().into(), 484 did: Did::new_unchecked(&did), 485 did_doc: state.did_resolver.resolve_did_document(&did).await, 486 access_jwt: access_meta.token, 487 refresh_jwt: refresh_meta.token, 488 verification_required: false, 489 verification_channel: "email".to_string(), 490 }), 491 ) 492 .into_response(); 493 } 494 Err(tranquil_db_traits::MigrationReactivationError::NotFound) => {} 495 Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated) => { 496 return ApiError::AccountAlreadyExists.into_response(); 497 } 498 Err(tranquil_db_traits::MigrationReactivationError::HandleTaken) => { 499 return ApiError::HandleTaken.into_response(); 500 } 501 Err(e) => { 502 error!("Error reactivating migration account: {:?}", e); 503 return ApiError::InternalError(None).into_response(); 504 } 505 } 506 } 507 508 let handle_typed = Handle::new_unchecked(&handle); 509 let handle_available = match state 510 .user_repo 511 .check_handle_available_for_new_account(&handle_typed) 512 .await 513 { 514 Ok(available) => available, 515 Err(e) => { 516 error!("Error checking handle availability: {:?}", e); 517 return ApiError::InternalError(None).into_response(); 518 } 519 }; 520 if !handle_available { 521 return ApiError::HandleTaken.into_response(); 522 } 523 524 let invite_code_required = std::env::var("INVITE_CODE_REQUIRED") 525 .map(|v| v == "true" || v == "1") 526 .unwrap_or(false); 527 if invite_code_required 528 && input 529 .invite_code 530 .as_ref() 531 .map(|c| c.trim().is_empty()) 532 .unwrap_or(true) 533 { 534 return ApiError::InviteCodeRequired.into_response(); 535 } 536 if let Some(code) = &input.invite_code 537 && !code.trim().is_empty() 538 { 539 let valid = match state.user_repo.check_and_consume_invite_code(code).await { 540 Ok(v) => v, 541 Err(e) => { 542 error!("Error checking invite code: {:?}", e); 543 return ApiError::InternalError(None).into_response(); 544 } 545 }; 546 if !valid { 547 return ApiError::InvalidInviteCode.into_response(); 548 } 549 } 550 551 if let Err(e) = validate_password(&input.password) { 552 return ApiError::InvalidRequest(e.to_string()).into_response(); 553 } 554 555 let password_clone = input.password.clone(); 556 let password_hash = 557 match tokio::task::spawn_blocking(move || hash(&password_clone, DEFAULT_COST)).await { 558 Ok(Ok(h)) => h, 559 Ok(Err(e)) => { 560 error!("Error hashing password: {:?}", e); 561 return ApiError::InternalError(None).into_response(); 562 } 563 Err(e) => { 564 error!("Failed to spawn blocking task: {:?}", e); 565 return ApiError::InternalError(None).into_response(); 566 } 567 }; 568 569 let deactivated_at: Option<chrono::DateTime<chrono::Utc>> = if is_migration || is_did_web_byod { 570 Some(chrono::Utc::now()) 571 } else { 572 None 573 }; 574 575 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 576 Ok(enc) => enc, 577 Err(e) => { 578 error!("Error encrypting user key: {:?}", e); 579 return ApiError::InternalError(None).into_response(); 580 } 581 }; 582 583 let mst = Mst::new(Arc::new(state.block_store.clone())); 584 let mst_root = match mst.persist().await { 585 Ok(c) => c, 586 Err(e) => { 587 error!("Error persisting MST: {:?}", e); 588 return ApiError::InternalError(None).into_response(); 589 } 590 }; 591 let rev = Tid::now(LimitedU32::MIN); 592 let did_for_commit = Did::new_unchecked(&did); 593 let (commit_bytes, _sig) = 594 match create_signed_commit(&did_for_commit, mst_root, rev.as_ref(), None, &signing_key) { 595 Ok(result) => result, 596 Err(e) => { 597 error!("Error creating genesis commit: {:?}", e); 598 return ApiError::InternalError(None).into_response(); 599 } 600 }; 601 let commit_cid = match state.block_store.put(&commit_bytes).await { 602 Ok(c) => c, 603 Err(e) => { 604 error!("Error saving genesis commit: {:?}", e); 605 return ApiError::InternalError(None).into_response(); 606 } 607 }; 608 let commit_cid_str = commit_cid.to_string(); 609 let rev_str = rev.as_ref().to_string(); 610 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; 611 612 let birthdate_pref = std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").ok().map(|_| { 613 json!({ 614 "$type": "app.bsky.actor.defs#personalDetailsPref", 615 "birthDate": "1998-05-06T00:00:00.000Z" 616 }) 617 }); 618 619 let preferred_comms_channel = match verification_channel { 620 "email" => tranquil_db_traits::CommsChannel::Email, 621 "discord" => tranquil_db_traits::CommsChannel::Discord, 622 "telegram" => tranquil_db_traits::CommsChannel::Telegram, 623 "signal" => tranquil_db_traits::CommsChannel::Signal, 624 _ => tranquil_db_traits::CommsChannel::Email, 625 }; 626 627 let create_input = tranquil_db_traits::CreatePasswordAccountInput { 628 handle: Handle::new_unchecked(&handle), 629 email: email.clone(), 630 did: Did::new_unchecked(&did), 631 password_hash, 632 preferred_comms_channel, 633 discord_id: input 634 .discord_id 635 .as_deref() 636 .map(|s| s.trim()) 637 .filter(|s| !s.is_empty()) 638 .map(String::from), 639 telegram_username: input 640 .telegram_username 641 .as_deref() 642 .map(|s| s.trim()) 643 .filter(|s| !s.is_empty()) 644 .map(String::from), 645 signal_number: input 646 .signal_number 647 .as_deref() 648 .map(|s| s.trim()) 649 .filter(|s| !s.is_empty()) 650 .map(String::from), 651 deactivated_at, 652 encrypted_key_bytes, 653 encryption_version: crate::config::ENCRYPTION_VERSION, 654 reserved_key_id, 655 commit_cid: commit_cid_str.clone(), 656 repo_rev: rev_str.clone(), 657 genesis_block_cids, 658 invite_code: input.invite_code.clone(), 659 birthdate_pref, 660 }; 661 662 let create_result = match state.user_repo.create_password_account(&create_input).await { 663 Ok(r) => r, 664 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 665 return ApiError::HandleNotAvailable(None).into_response(); 666 } 667 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 668 return ApiError::EmailTaken.into_response(); 669 } 670 Err(tranquil_db_traits::CreateAccountError::DidExists) => { 671 return ApiError::AccountAlreadyExists.into_response(); 672 } 673 Err(e) => { 674 error!("Error creating password account: {:?}", e); 675 return ApiError::InternalError(None).into_response(); 676 } 677 }; 678 let user_id = create_result.user_id; 679 if !is_migration && !is_did_web_byod { 680 let did_typed = Did::new_unchecked(&did); 681 let handle_typed = Handle::new_unchecked(&handle); 682 if let Err(e) = crate::api::repo::record::sequence_identity_event( 683 &state, 684 &did_typed, 685 Some(&handle_typed), 686 ) 687 .await 688 { 689 warn!("Failed to sequence identity event for {}: {}", did, e); 690 } 691 if let Err(e) = 692 crate::api::repo::record::sequence_account_event(&state, &did_typed, true, None).await 693 { 694 warn!("Failed to sequence account event for {}: {}", did, e); 695 } 696 if let Err(e) = crate::api::repo::record::sequence_genesis_commit( 697 &state, 698 &did_typed, 699 &commit_cid, 700 &mst_root, 701 &rev_str, 702 ) 703 .await 704 { 705 warn!("Failed to sequence commit event for {}: {}", did, e); 706 } 707 if let Err(e) = crate::api::repo::record::sequence_sync_event( 708 &state, 709 &did_typed, 710 &commit_cid_str, 711 Some(rev.as_ref()), 712 ) 713 .await 714 { 715 warn!("Failed to sequence sync event for {}: {}", did, e); 716 } 717 let profile_record = json!({ 718 "$type": "app.bsky.actor.profile", 719 "displayName": input.handle 720 }); 721 let profile_collection = Nsid::new_unchecked("app.bsky.actor.profile"); 722 let profile_rkey = Rkey::new_unchecked("self"); 723 if let Err(e) = crate::api::repo::record::create_record_internal( 724 &state, 725 &did_typed, 726 &profile_collection, 727 &profile_rkey, 728 &profile_record, 729 ) 730 .await 731 { 732 warn!("Failed to create default profile for {}: {}", did, e); 733 } 734 } 735 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 736 if !is_migration { 737 if let Some(ref recipient) = verification_recipient { 738 let verification_token = crate::auth::verification_token::generate_signup_token( 739 &did, 740 verification_channel, 741 recipient, 742 ); 743 let formatted_token = 744 crate::auth::verification_token::format_token_for_display(&verification_token); 745 if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( 746 state.infra_repo.as_ref(), 747 user_id, 748 verification_channel, 749 recipient, 750 &formatted_token, 751 &hostname, 752 ) 753 .await 754 { 755 warn!( 756 "Failed to enqueue signup verification notification: {:?}", 757 e 758 ); 759 } 760 } 761 } else if let Some(ref user_email) = email { 762 let token = crate::auth::verification_token::generate_migration_token(&did, user_email); 763 let formatted_token = crate::auth::verification_token::format_token_for_display(&token); 764 if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification( 765 state.user_repo.as_ref(), 766 state.infra_repo.as_ref(), 767 user_id, 768 user_email, 769 &formatted_token, 770 &hostname, 771 ) 772 .await 773 { 774 warn!("Failed to enqueue migration verification email: {:?}", e); 775 } 776 } 777 778 let access_meta = match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) 779 { 780 Ok(m) => m, 781 Err(e) => { 782 error!("createAccount: Error creating access token: {:?}", e); 783 return ApiError::InternalError(None).into_response(); 784 } 785 }; 786 let refresh_meta = 787 match crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) { 788 Ok(m) => m, 789 Err(e) => { 790 error!("createAccount: Error creating refresh token: {:?}", e); 791 return ApiError::InternalError(None).into_response(); 792 } 793 }; 794 let session_data = tranquil_db_traits::SessionTokenCreate { 795 did: Did::new_unchecked(&did), 796 access_jti: access_meta.jti.clone(), 797 refresh_jti: refresh_meta.jti.clone(), 798 access_expires_at: access_meta.expires_at, 799 refresh_expires_at: refresh_meta.expires_at, 800 legacy_login: false, 801 mfa_verified: false, 802 scope: None, 803 controller_did: None, 804 app_password_name: None, 805 }; 806 if let Err(e) = state.session_repo.create_session(&session_data).await { 807 error!("createAccount: Error creating session: {:?}", e); 808 return ApiError::InternalError(None).into_response(); 809 } 810 811 let did_doc = state.did_resolver.resolve_did_document(&did).await; 812 813 if is_migration { 814 info!( 815 "[MIGRATION] createAccount: SUCCESS - Account ready for migration did={} handle={}", 816 did, handle 817 ); 818 } 819 820 ( 821 StatusCode::OK, 822 Json(CreateAccountOutput { 823 handle: handle.clone().into(), 824 did: Did::new_unchecked(&did), 825 did_doc, 826 access_jwt: access_meta.token, 827 refresh_jwt: refresh_meta.token, 828 verification_required: !is_migration, 829 verification_channel: verification_channel.to_string(), 830 }), 831 ) 832 .into_response() 833}