forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
32 kB
833 lines
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}