diff --git a/crates/tranquil-api/src/admin/account/update.rs b/crates/tranquil-api/src/admin/account/update.rs index 489d20b..a0a1bb8 100644 --- a/crates/tranquil-api/src/admin/account/update.rs +++ b/crates/tranquil-api/src/admin/account/update.rs @@ -106,7 +106,7 @@ pub async fn update_account_handle( } let _ = state .cache - .delete(&tranquil_pds::cache_keys::handle_key(&handle)) + .delete(&tranquil_pds::cache_keys::handle_key(&handle_for_check)) .await; if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event( &state, diff --git a/crates/tranquil-api/src/delegation.rs b/crates/tranquil-api/src/delegation.rs index 241c265..1bfd0e0 100644 --- a/crates/tranquil-api/src/delegation.rs +++ b/crates/tranquil-api/src/delegation.rs @@ -41,8 +41,7 @@ pub async fn list_controllers( .fetch_did_document(c.did.as_str()) .await .ok() - .and_then(|doc| tranquil_types::did_doc::extract_handle(&doc)) - .map(Into::into); + .and_then(|doc| tranquil_types::did_doc::extract_handle(&doc)); } c } @@ -380,7 +379,6 @@ pub async fn create_delegated_account( e })?; let did = plc.did; - let handle: Handle = handle.parse().map_err(|_| ApiError::InvalidHandle(None))?; info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account"); let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?; @@ -465,14 +463,13 @@ pub async fn resolve_controller( .parse() .map_err(|_| ApiError::ControllerNotFound)? } else { - let local_handle: Option = identifier.parse().ok(); - let local_user = match local_handle { - Some(ref h) => state.repos.user.get_by_handle(h).await.ok().flatten(), - None => None, - }; + let handle: Handle = identifier + .parse() + .map_err(|_| ApiError::ControllerNotFound)?; + let local_user = state.repos.user.get_by_handle(&handle).await.ok().flatten(); match local_user { Some(user) => user.did, - None => tranquil_pds::handle::resolve_handle(identifier) + None => tranquil_pds::handle::resolve_handle(&handle) .await .map_err(|_| ApiError::ControllerNotFound)? .parse() diff --git a/crates/tranquil-api/src/discord_webhook.rs b/crates/tranquil-api/src/discord_webhook.rs index f038838..2740750 100644 --- a/crates/tranquil-api/src/discord_webhook.rs +++ b/crates/tranquil-api/src/discord_webhook.rs @@ -168,10 +168,11 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response { "Received /start from Discord user" ); + let handle = handle.map(Handle::from); match state .repos .user - .store_discord_user_id(&discord_username, &discord_user_id, handle.as_deref()) + .store_discord_user_id(&discord_username, &discord_user_id, handle.as_ref()) .await { Ok(Some(user_id)) => { diff --git a/crates/tranquil-api/src/identity/account.rs b/crates/tranquil-api/src/identity/account.rs index 4211b9e..4d5635c 100644 --- a/crates/tranquil-api/src/identity/account.rs +++ b/crates/tranquil-api/src/identity/account.rs @@ -48,22 +48,14 @@ pub struct CreateAccountOutput { async fn try_reactivate_migration( state: &AppState, did: &str, - handle: &str, + handle: &Handle, email: &Option, verification_channel: tranquil_db_traits::CommsChannel, verification_recipient: Option<&str>, ) -> Option { - let did_typed: Did = match did.parse() { - Ok(d) => d, - Err(_) => return Some(ApiError::InternalError(Some("Invalid DID".into())).into_response()), - }; - let handle_typed: Handle = match handle.parse() { - Ok(h) => h, - Err(_) => return Some(ApiError::InvalidHandle(None).into_response()), - }; let reactivate_input = tranquil_db_traits::MigrationReactivationInput { - did: did_typed.clone(), - new_handle: handle_typed.clone(), + did: did.clone(), + new_handle: handle.clone(), new_email: email.clone(), }; match state @@ -153,8 +145,8 @@ async fn try_reactivate_migration( ( StatusCode::OK, Json(CreateAccountOutput { - handle: handle.to_string().into(), - did: did_typed.clone(), + handle: handle.clone(), + did: did.clone(), did_doc: state .did_resolver .fetch_did_document(did) @@ -342,7 +334,7 @@ pub async fn create_account( } if !is_did_web_byod && let Err(e) = - verify_did_web(d, hostname, &input.handle, input.signing_key.as_deref()).await + verify_did_web(d, hostname, &input.handle, input.signing_key.as_ref()).await { return ApiError::InvalidDid(e.to_string()).into_response(); } @@ -357,7 +349,7 @@ pub async fn create_account( } else if d.starts_with("did:web:") { if !is_did_web_byod && let Err(e) = - verify_did_web(d, hostname, &input.handle, input.signing_key.as_deref()) + verify_did_web(d, hostname, &input.handle, input.signing_key.as_ref()) .await { return ApiError::InvalidDid(e.to_string()).into_response(); @@ -397,14 +389,10 @@ pub async fn create_account( return response; } - let handle_typed: Handle = match handle.parse() { - Ok(h) => h, - Err(_) => return ApiError::InvalidHandle(None).into_response(), - }; let handle_available = match state .repos .user - .check_handle_available_for_new_account(&handle_typed) + .check_handle_available_for_new_account(&handle) .await { Ok(available) => available, @@ -466,7 +454,7 @@ pub async fn create_account( let repo_for_seq = repo.clone(); let create_input = tranquil_db_traits::CreatePasswordAccountInput { - handle: handle_typed.clone(), + handle: handle.clone(), email: email.clone(), did: did_for_commit.clone(), password_hash, @@ -512,14 +500,8 @@ pub async fn create_account( }; let user_id = create_result.user_id; if !is_migration && !is_did_web_byod { - super::provision::sequence_new_account( - &state, - &did_for_commit, - &handle_typed, - &repo_for_seq, - &input.handle, - ) - .await; + super::provision::sequence_new_account(&state, &did, &handle, &repo_for_seq, &input.handle) + .await; } if !is_migration { if let Some(ref recipient) = verification_recipient { @@ -569,8 +551,8 @@ pub async fn create_account( ( StatusCode::OK, Json(CreateAccountOutput { - handle: handle.clone().into(), - did: did_for_commit, + handle: handle.clone(), + did, did_doc: did_doc.map(|f| (*f).clone()), access_jwt: session.access_jwt, refresh_jwt: session.refresh_jwt, diff --git a/crates/tranquil-api/src/identity/did.rs b/crates/tranquil-api/src/identity/did.rs index e5adcb5..be17182 100644 --- a/crates/tranquil-api/src/identity/did.rs +++ b/crates/tranquil-api/src/identity/did.rs @@ -19,7 +19,7 @@ use tranquil_pds::rate_limit::{ HandleUpdateDailyLimit, HandleUpdateLimit, check_user_rate_limit_with_message, }; use tranquil_pds::state::AppState; -use tranquil_pds::types::Handle; +use tranquil_pds::types::{Did, Handle}; use tranquil_pds::util::get_header_str; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -54,6 +54,10 @@ pub async fn resolve_handle( return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); } }; + let cache_key = tranquil_pds::cache_keys::handle_key(&handle); + if let Some(did) = state.cache.get(&cache_key).await { + return DidResponse::response(did).into_response(); + } let user = state.repos.user.get_by_handle(&handle).await; match user { Ok(Some(row)) => { @@ -63,7 +67,7 @@ pub async fn resolve_handle( .await; DidResponse::response(row.did).into_response() } - Ok(None) => match tranquil_pds::handle::resolve_handle(handle.as_str()).await { + Ok(None) => match tranquil_pds::handle::resolve_handle(&handle).await { Ok(did) => { let _ = state .cache @@ -212,8 +216,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> pub async fn user_did_doc(State(state): State, Path(handle): Path) -> Response { let hostname = &tranquil_config::get().server.hostname; let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); - let current_handle = format!("{}.{}", handle, hostname_for_handles); - let current_handle_typed: Handle = match current_handle.parse() { + let current_handle: Handle = match format!("{}.{}", handle, hostname_for_handles).parse() { Ok(h) => h, Err(_) => { return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); @@ -583,7 +586,7 @@ pub async fn update_handle( .max_by_key(|d| d.len()) .cloned(); let is_domain_itself = handle_domains.iter().any(|d| d == &new_handle); - let handle = if (!new_handle.contains('.') || matched_handle_domain.is_some()) + let handle: Handle = if (!new_handle.contains('.') || matched_handle_domain.is_some()) && !is_domain_itself { let (short_part, full_handle) = match &matched_handle_domain { @@ -598,13 +601,12 @@ pub async fn update_handle( } }; if full_handle == current_handle { - let handle_typed: Handle = match full_handle.parse() { + let handle: Handle = match full_handle.parse() { Ok(h) => h, Err(_) => return Err(ApiError::InvalidHandle(None)), }; if let Err(e) = - tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)) - .await + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await { warn!("Failed to sequence identity event for handle update: {}", e); } @@ -622,21 +624,25 @@ pub async fn update_handle( return Err(ApiError::InvalidHandle(Some("Handle too long".into()))); } full_handle + .parse() + .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))? } else { + let handle: Handle = new_handle + .parse() + .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?; if new_handle == current_handle { let handle_typed: Handle = match new_handle.parse() { Ok(h) => h, Err(_) => return Err(ApiError::InvalidHandle(None)), }; if let Err(e) = - tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)) - .await + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await { warn!("Failed to sequence identity event for handle update: {}", e); } return Ok(Json(EmptyResponse {})); } - match tranquil_pds::handle::verify_handle_ownership(&new_handle, &did).await { + match tranquil_pds::handle::verify_handle_ownership(&handle, &did).await { Ok(()) => {} Err(tranquil_pds::handle::HandleResolutionError::NotFound) => { return Err(ApiError::HandleNotAvailable(None)); @@ -655,15 +661,12 @@ pub async fn update_handle( )))); } } - new_handle.clone() + handle }; - let handle_typed: Handle = handle - .parse() - .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?; let handle_exists = state .repos .user - .check_handle_exists(&handle_typed, user_id) + .check_handle_exists(&handle, user_id) .await .log_db_err("checking handle existence")?; if handle_exists { @@ -672,7 +675,7 @@ pub async fn update_handle( state .repos .user - .update_handle(user_id, &handle_typed) + .update_handle(user_id, &handle) .await .map_err(|e| { error!("DB error updating handle: {:?}", e); @@ -690,11 +693,11 @@ pub async fn update_handle( .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; if let Err(e) = - tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)).await + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await { warn!("Failed to sequence identity event for handle update: {}", e); } - if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { + if let Err(e) = update_plc_handle(&state, &did, &handle).await { warn!("Failed to update PLC handle: {}", e); } Ok(Json(EmptyResponse {})) diff --git a/crates/tranquil-api/src/identity/handle.rs b/crates/tranquil-api/src/identity/handle.rs index 60c7e2a..75bde49 100644 --- a/crates/tranquil-api/src/identity/handle.rs +++ b/crates/tranquil-api/src/identity/handle.rs @@ -26,10 +26,9 @@ pub async fn verify_handle_ownership( _rate_limit: RateLimited, Json(input): Json, ) -> Response { - let handle_str = input.handle.as_str(); let did_str = input.did.as_str(); - let dns_mismatch = match tranquil_pds::handle::resolve_handle_dns(handle_str).await { + let dns_mismatch = match tranquil_pds::handle::resolve_handle_dns(&input.handle).await { Ok(did) if did == did_str => { return Json(VerifyHandleOwnershipOutput { verified: true, @@ -45,7 +44,7 @@ pub async fn verify_handle_ownership( Err(_) => None, }; - match tranquil_pds::handle::resolve_handle_http(handle_str).await { + match tranquil_pds::handle::resolve_handle_http(&input.handle).await { Ok(did) if did == did_str => Json(VerifyHandleOwnershipOutput { verified: true, method: Some("http".to_string()), diff --git a/crates/tranquil-api/src/identity/provision.rs b/crates/tranquil-api/src/identity/provision.rs index 83eba2d..d5923d9 100644 --- a/crates/tranquil-api/src/identity/provision.rs +++ b/crates/tranquil-api/src/identity/provision.rs @@ -14,7 +14,7 @@ pub struct PlcDidResult { pub signing_key: SigningKey, } -pub async fn create_plc_did(state: &AppState, handle: &str) -> Result { +pub async fn create_plc_did(state: &AppState, handle: &Handle) -> Result { use k256::SecretKey; use rand::rngs::OsRng; @@ -25,10 +25,7 @@ pub async fn create_plc_did(state: &AppState, handle: &str) -> Result Result Result { let hostname = &tranquil_config::get().server.hostname; let pds_endpoint = format!("https://{}", hostname); diff --git a/crates/tranquil-api/src/notification_prefs.rs b/crates/tranquil-api/src/notification_prefs.rs index 323bee7..a8408a9 100644 --- a/crates/tranquil-api/src/notification_prefs.rs +++ b/crates/tranquil-api/src/notification_prefs.rs @@ -6,7 +6,7 @@ use tranquil_db_traits::{CommsChannel, CommsStatus, CommsType}; use tranquil_pds::api::error::{ApiError, DbResultExt}; use tranquil_pds::auth::{Active, Auth}; use tranquil_pds::state::AppState; -use tranquil_types::Did; +use tranquil_types::{Did, Handle}; #[derive(Serialize)] #[serde(rename_all = "camelCase")] @@ -137,7 +137,7 @@ pub async fn request_channel_verification( did: &Did, channel: CommsChannel, identifier: &str, - handle: Option<&str>, + handle: Option<&Handle>, ) -> Result { let token = tranquil_pds::auth::verification_token::generate_channel_update_token( did, channel, identifier, @@ -147,12 +147,12 @@ pub async fn request_channel_verification( match channel { CommsChannel::Email => { let hostname = &tranquil_config::get().server.hostname; - let handle_str = handle.unwrap_or("user"); + let fallback_handle = Handle::from("user".to_string()); tranquil_pds::comms::comms_repo::enqueue_email_update( state.repos.infra.as_ref(), user_id, identifier, - handle_str, + handle.unwrap_or(&fallback_handle), &formatted_token, hostname, ) diff --git a/crates/tranquil-api/src/server/account_status.rs b/crates/tranquil-api/src/server/account_status.rs index 16a9517..2994919 100644 --- a/crates/tranquil-api/src/server/account_status.rs +++ b/crates/tranquil-api/src/server/account_status.rs @@ -423,10 +423,8 @@ pub async fn activate_account( "[MIGRATION] activateAccount: Sequencing identity event for did={} handle={:?}", did, handle ); - let handle_typed = handle.clone(); if let Err(e) = - tranquil_pds::repo_ops::sequence_identity_event(&state, &did, handle_typed.as_ref()) - .await + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, handle.as_ref()).await { warn!( "[MIGRATION] activateAccount: Failed to sequence identity event for activation: {}", diff --git a/crates/tranquil-api/src/server/passkey_account.rs b/crates/tranquil-api/src/server/passkey_account.rs index 550ed53..e13e0b9 100644 --- a/crates/tranquil-api/src/server/passkey_account.rs +++ b/crates/tranquil-api/src/server/passkey_account.rs @@ -294,10 +294,6 @@ pub async fn create_passkey_account( None }; - let handle_typed: Handle = match handle.parse() { - Ok(h) => h, - Err(_) => return Err(ApiError::InvalidHandle(None)), - }; let repo_for_seq = repo.clone(); let comms = crate::identity::provision::normalize_comms_usernames( input.discord_username.as_deref(), @@ -305,7 +301,7 @@ pub async fn create_passkey_account( input.signal_username.as_deref(), ); let create_input = tranquil_db_traits::CreatePasskeyAccountInput { - handle: handle_typed.clone(), + handle: handle.clone(), email: email.clone().unwrap_or_default(), did: did_typed.clone(), preferred_comms_channel: verification_channel, @@ -346,10 +342,10 @@ pub async fn create_passkey_account( if !is_byod_did_web { crate::identity::provision::sequence_new_account( &state, - &did_typed, - &handle_typed, - &repo_for_seq, + &did, &handle, + &repo_for_seq, + handle.as_str(), ) .await; } @@ -398,8 +394,8 @@ pub async fn create_passkey_account( }; Ok(Json(CreatePasskeyAccountOutput { - did: did.into(), - handle: handle.into(), + did, + handle, setup_token, setup_expires_at, access_jwt, diff --git a/crates/tranquil-api/src/server/password.rs b/crates/tranquil-api/src/server/password.rs index 393afbf..68c3788 100644 --- a/crates/tranquil-api/src/server/password.rs +++ b/crates/tranquil-api/src/server/password.rs @@ -10,7 +10,7 @@ use tranquil_pds::auth::{ }; use tranquil_pds::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit}; use tranquil_pds::state::AppState; -use tranquil_pds::types::PlainPassword; +use tranquil_pds::types::{Handle, PlainPassword}; use tranquil_pds::validation::validate_password; #[derive(Deserialize)] @@ -48,7 +48,10 @@ pub async fn request_password_reset( let user_id = match state .repos .user - .get_id_by_email_or_handle(normalized, normalized_handle.as_str()) + .get_id_by_email_or_handle( + normalized, + &Handle::from(normalized_handle.as_str().to_string()), + ) .await { Ok(Some(id)) => id, diff --git a/crates/tranquil-api/src/server/session.rs b/crates/tranquil-api/src/server/session.rs index 9246a0b..c38874e 100644 --- a/crates/tranquil-api/src/server/session.rs +++ b/crates/tranquil-api/src/server/session.rs @@ -20,7 +20,7 @@ use tranquil_pds::rate_limit::{ check_user_rate_limit_with_message, }; use tranquil_pds::state::AppState; -use tranquil_pds::types::{AccountState, Did, Handle, PlainPassword}; +use tranquil_pds::types::{AccountState, AtIdentifier, Did, Handle, PlainPassword}; use tranquil_types::TokenId; pub fn verification_blocks_login(channel_verification: &ChannelVerificationStatus) -> bool { @@ -78,6 +78,16 @@ pub async fn create_session( "Normalized identifier: {} -> {}", input.identifier, normalized_identifier ); + let Ok(login_identifier) = AtIdentifier::new(normalized_identifier.as_str()) else { + let _ = verify( + &input.password, + "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", + ); + warn!("Login identifier is not a valid handle or DID"); + return Err(ApiError::AuthenticationFailed(Some( + "Invalid identifier or password".into(), + ))); + }; let row = match state .repos .user diff --git a/crates/tranquil-api/src/telegram_webhook.rs b/crates/tranquil-api/src/telegram_webhook.rs index 32d2821..004f2a8 100644 --- a/crates/tranquil-api/src/telegram_webhook.rs +++ b/crates/tranquil-api/src/telegram_webhook.rs @@ -62,7 +62,8 @@ pub async fn handle_telegram_webhook( && let Some(from) = message.from && let Some(username) = from.username { - let handle = parse_start_handle(message.text.as_deref()); + let handle = + parse_start_handle(message.text.as_deref()).map(tranquil_types::Handle::from); debug!( telegram_username = %username, @@ -73,7 +74,7 @@ pub async fn handle_telegram_webhook( match state .repos .user - .store_telegram_chat_id(&username, from.id, handle.as_deref()) + .store_telegram_chat_id(&username, from.id, handle.as_ref()) .await { Ok(Some(user_id)) => { diff --git a/crates/tranquil-db-traits/src/user.rs b/crates/tranquil-db-traits/src/user.rs index 62aa2ff..68a8a6b 100644 --- a/crates/tranquil-db-traits/src/user.rs +++ b/crates/tranquil-db-traits/src/user.rs @@ -264,7 +264,7 @@ pub trait UserRepository: Send + Sync { &self, telegram_username: &str, chat_id: i64, - handle: Option<&str>, + handle: Option<&Handle>, ) -> Result, DbError>; async fn get_telegram_chat_id(&self, user_id: Uuid) -> Result, DbError>; @@ -279,7 +279,7 @@ pub trait UserRepository: Send + Sync { &self, discord_username: &str, discord_id: &str, - handle: Option<&str>, + handle: Option<&Handle>, ) -> Result, DbError>; async fn get_verification_info( @@ -442,7 +442,7 @@ pub trait UserRepository: Send + Sync { async fn get_id_by_email_or_handle( &self, email: &str, - handle: &str, + handle: &Handle, ) -> Result, DbError>; async fn count_accounts_by_email(&self, email: &str) -> Result; diff --git a/crates/tranquil-db/src/postgres/user.rs b/crates/tranquil-db/src/postgres/user.rs index aec0535..1d650aa 100644 --- a/crates/tranquil-db/src/postgres/user.rs +++ b/crates/tranquil-db/src/postgres/user.rs @@ -1732,12 +1732,12 @@ impl UserRepository for PostgresUserRepository { async fn get_id_by_email_or_handle( &self, email: &str, - handle: &str, + handle: &Handle, ) -> Result, DbError> { sqlx::query_scalar!( "SELECT id FROM users WHERE LOWER(email) = $1 OR handle = $2", email, - handle + handle.as_str() ) .fetch_optional(&self.pool) .await @@ -3225,7 +3225,7 @@ impl UserRepository for PostgresUserRepository { &self, discord_username: &str, discord_id: &str, - handle: Option<&str>, + handle: Option<&Handle>, ) -> Result, DbError> { let result = match handle { Some(h) => sqlx::query_scalar!( @@ -3287,7 +3287,7 @@ impl UserRepository for PostgresUserRepository { &self, telegram_username: &str, chat_id: i64, - handle: Option<&str>, + handle: Option<&Handle>, ) -> Result, DbError> { let result = match handle { Some(h) => sqlx::query_scalar!( diff --git a/crates/tranquil-oauth-server/src/sso_endpoints.rs b/crates/tranquil-oauth-server/src/sso_endpoints.rs index 89e39e6..0c34ff2 100644 --- a/crates/tranquil-oauth-server/src/sso_endpoints.rs +++ b/crates/tranquil-oauth-server/src/sso_endpoints.rs @@ -797,7 +797,7 @@ pub async fn check_handle_available( } let domain = query.domain.as_deref().unwrap_or(&available_domains[0]); let full_handle = format!("{}.{}", validated, domain); - let handle_typed: tranquil_pds::types::Handle = match full_handle.parse() { + let handle: tranquil_pds::types::Handle = match full_handle.parse() { Ok(h) => h, Err(_) => return Err(ApiError::InvalidHandle(None)), }; @@ -805,7 +805,7 @@ pub async fn check_handle_available( let db_available = state .repos .user - .check_handle_available_for_new_account(&handle_typed) + .check_handle_available_for_new_account(&handle) .await .unwrap_or(false); @@ -840,7 +840,7 @@ pub struct CompleteRegistrationInput { #[serde(rename_all = "camelCase")] pub struct CompleteRegistrationResponse { pub did: String, - pub handle: String, + pub handle: tranquil_pds::types::Handle, pub redirect_url: String, #[serde(skip_serializing_if = "Option::is_none")] pub access_jwt: Option, @@ -889,24 +889,27 @@ pub async fn complete_registration( .filter(|d| input.handle.ends_with(&format!(".{}", d))) .max_by_key(|d| d.len()); - let handle = if !input.handle.contains('.') || matched_domain.is_some() { - let handle_to_validate = match matched_domain { - Some(domain) => input - .handle - .strip_suffix(&format!(".{}", domain)) - .unwrap_or(&input.handle), - None => &input.handle, + let handle: tranquil_pds::types::Handle = + if !input.handle.contains('.') || matched_domain.is_some() { + let handle_to_validate = match matched_domain { + Some(domain) => input + .handle + .strip_suffix(&format!(".{}", domain)) + .unwrap_or(&input.handle), + None => &input.handle, + }; + match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) { + Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])) + .parse() + .map_err(|_| ApiError::InvalidHandle(None))?, + Err(_) => return Err(ApiError::InvalidHandle(None)), + } + } else { + match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) { + Ok(h) => h, + Err(_) => return Err(ApiError::InvalidHandle(None)), + } }; - match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) { - Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])), - Err(_) => return Err(ApiError::InvalidHandle(None)), - } - } else { - match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) { - Ok(h) => h, - Err(_) => return Err(ApiError::InvalidHandle(None)), - } - }; let verification_channel = input .verification_channel @@ -989,12 +992,10 @@ pub async fn complete_registration( let invite_registration = check_registration_invite(&state, input.invite_code.as_deref()).await?; - let handle_typed: tranquil_pds::types::Handle = - handle.parse().map_err(|_| ApiError::InvalidHandle(None))?; let reserved = state .repos .user - .reserve_handle(&handle_typed, client_ip) + .reserve_handle(&handle, client_ip) .await .unwrap_or(false); @@ -1130,7 +1131,7 @@ pub async fn complete_registration( }; let create_input = tranquil_db_traits::CreateSsoAccountInput { - handle: handle_typed.clone(), + handle: handle.clone(), email: email.clone(), did: did_typed.clone(), preferred_comms_channel: verification_channel, @@ -1190,15 +1191,10 @@ pub async fn complete_registration( } }; - let _ = state - .repos - .user - .release_handle_reservation(&handle_typed) - .await; + let _ = state.repos.user.release_handle_reservation(&handle).await; if let Err(e) = - tranquil_pds::repo_ops::sequence_identity_event(&state, &did_typed, Some(&handle_typed)) - .await + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await { tracing::warn!("Failed to sequence identity event for {}: {}", did, e); } @@ -1214,7 +1210,7 @@ pub async fn complete_registration( let profile_record = json!({ "$type": "app.bsky.actor.profile", - "displayName": handle_typed.as_str() + "displayName": handle.as_str() }); if let Err(e) = tranquil_pds::repo_ops::create_record_internal( &state, diff --git a/crates/tranquil-pds/src/api/validation.rs b/crates/tranquil-pds/src/api/validation.rs index 8d16c11..6975141 100644 --- a/crates/tranquil-pds/src/api/validation.rs +++ b/crates/tranquil-pds/src/api/validation.rs @@ -1,3 +1,4 @@ +use crate::types::Handle; use std::fmt; pub const MAX_EMAIL_LENGTH: usize = 254; @@ -146,7 +147,7 @@ pub enum ReservedHandlePolicy { Reject, } -pub fn validate_full_domain_handle(handle: &str) -> Result { +pub fn validate_full_domain_handle(handle: &str) -> Result { let handle = handle.trim(); if handle.is_empty() { @@ -189,14 +190,14 @@ pub fn validate_full_domain_handle(handle: &str) -> Result Result { validate_service_handle(handle, ReservedHandlePolicy::Reject) } -pub fn resolve_handle_input(input: &str) -> Result { +pub fn resolve_handle_input(input: &str) -> Result { let available_domains = tranquil_config::get().server.available_user_domain_list(); let matched_domain = available_domains .iter() @@ -209,7 +210,7 @@ pub fn resolve_handle_input(input: &str) -> Result input, }; let validated = validate_short_handle(handle_to_validate)?; - Ok(format!( + Ok(Handle::from(format!( "{}.{}", validated, matched_domain.unwrap_or(&available_domains[0]) diff --git a/crates/tranquil-pds/src/cache_keys.rs b/crates/tranquil-pds/src/cache_keys.rs index f0d1349..b5f3951 100644 --- a/crates/tranquil-pds/src/cache_keys.rs +++ b/crates/tranquil-pds/src/cache_keys.rs @@ -12,7 +12,7 @@ pub fn user_status_key(did: &str) -> String { format!("auth:status:{}", did) } -pub fn handle_key(handle: &str) -> String { +pub fn handle_key(handle: &Handle) -> String { format!("handle:{}", handle) } diff --git a/crates/tranquil-pds/src/comms/service.rs b/crates/tranquil-pds/src/comms/service.rs index d9f23b2..0f0303c 100644 --- a/crates/tranquil-pds/src/comms/service.rs +++ b/crates/tranquil-pds/src/comms/service.rs @@ -308,7 +308,7 @@ pub mod repo { infra_repo: &dyn InfraRepository, user_id: Uuid, new_email: &str, - handle: &str, + handle: &crate::types::Handle, code: &str, hostname: &str, ) -> Result { @@ -323,7 +323,7 @@ pub mod repo { let body = format_message( strings.email_update_body, &[ - ("handle", handle), + ("handle", handle.as_str()), ("code", code), ("verify_page", &verify_page), ("verify_link", &verify_link), diff --git a/crates/tranquil-pds/src/delegation/mod.rs b/crates/tranquil-pds/src/delegation/mod.rs index 598c56e..968f552 100644 --- a/crates/tranquil-pds/src/delegation/mod.rs +++ b/crates/tranquil-pds/src/delegation/mod.rs @@ -12,14 +12,14 @@ pub use tranquil_db_traits::DelegationActionType; use crate::did::DidResolutionError; use crate::state::AppState; -use crate::types::Did; +use crate::types::{Did, Handle}; #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct ResolvedIdentity { pub did: Did, #[serde(skip_serializing_if = "Option::is_none")] - pub handle: Option, + pub handle: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pds_url: Option, pub is_local: bool, @@ -52,7 +52,8 @@ pub async fn resolve_identity( let handle = did_doc .also_known_as .iter() - .find_map(|alias| alias.strip_prefix("at://").map(|s| s.to_string())); + .find_map(|alias| alias.strip_prefix("at://")) + .and_then(|s| Handle::new(s).ok()); Ok(ResolvedIdentity { did: did.clone(), diff --git a/crates/tranquil-pds/src/handle/mod.rs b/crates/tranquil-pds/src/handle/mod.rs index 89a90bf..37c2440 100644 --- a/crates/tranquil-pds/src/handle/mod.rs +++ b/crates/tranquil-pds/src/handle/mod.rs @@ -1,5 +1,6 @@ pub mod reserved; +use crate::types::{Did, Handle}; use hickory_resolver::TokioAsyncResolver; use hickory_resolver::config::{ResolverConfig, ResolverOpts}; use thiserror::Error; @@ -18,7 +19,7 @@ pub enum HandleResolutionError { DidMismatch { expected: String, actual: String }, } -pub async fn resolve_handle_dns(handle: &str) -> Result { +pub async fn resolve_handle_dns(handle: &Handle) -> Result { let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or_else(|e| { tracing::warn!("falling back to default DNS resolvers: {}", e); TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()) @@ -41,7 +42,7 @@ pub async fn resolve_handle_dns(handle: &str) -> Result Result { +pub async fn resolve_handle_http(handle: &Handle) -> Result { let url = format!("https://{}/.well-known/atproto-did", handle); let client = crate::api::proxy_client::handle_resolution_client(); let response = client @@ -65,7 +66,7 @@ pub async fn resolve_handle_http(handle: &str) -> Result Result { +pub async fn resolve_handle(handle: &Handle) -> Result { match resolve_handle_dns(handle).await { Ok(did) => return Ok(did), Err(e) => { @@ -76,7 +77,7 @@ pub async fn resolve_handle(handle: &str) -> Result Result<(), HandleResolutionError> { let resolved_did = resolve_handle(handle).await?; diff --git a/crates/tranquil-pds/src/plc/mod.rs b/crates/tranquil-pds/src/plc/mod.rs index eb8e390..2253e26 100644 --- a/crates/tranquil-pds/src/plc/mod.rs +++ b/crates/tranquil-pds/src/plc/mod.rs @@ -1,4 +1,5 @@ use crate::cache::Cache; +use crate::types::{Did, Handle}; use base32::Alphabet; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use k256::ecdsa::{Signature, SigningKey, signature::Signer}; @@ -518,7 +519,7 @@ pub struct GenesisResult { pub fn create_genesis_operation( signing_key: &SigningKey, configured_rotation_key: Option<&str>, - handle: &str, + handle: &Handle, pds_endpoint: &str, ) -> Result { let signing_did_key = signing_key_to_did_key(signing_key); @@ -780,9 +781,13 @@ mod tests { let key = SigningKey::random(&mut rand::thread_rng()); let signing_did_key = signing_key_to_did_key(&key); let operator_key = "did:key:zQ3shWhelkOperatorKey"; - let result = - create_genesis_operation(&key, Some(operator_key), "whelk.nel.pet", "https://nel.pet") - .unwrap(); + let result = create_genesis_operation( + &key, + Some(operator_key), + &crate::types::Handle::from("whelk.nel.pet".to_string()), + "https://nel.pet", + ) + .unwrap(); let rotation_keys = result.signed_operation["rotationKeys"] .as_array() .unwrap() diff --git a/crates/tranquil-pds/src/sync/frame.rs b/crates/tranquil-pds/src/sync/frame.rs index fdaa1b4..865fe3e 100644 --- a/crates/tranquil-pds/src/sync/frame.rs +++ b/crates/tranquil-pds/src/sync/frame.rs @@ -65,8 +65,8 @@ pub struct RepoOp { pub struct IdentityFrame { pub did: Did, #[serde(skip_serializing_if = "Option::is_none")] - pub handle: Option, - pub seq: i64, + pub handle: Option, + pub seq: SequenceNumber, pub time: String, } diff --git a/crates/tranquil-pds/src/sync/util.rs b/crates/tranquil-pds/src/sync/util.rs index 45c9207..2aa36d1 100644 --- a/crates/tranquil-pds/src/sync/util.rs +++ b/crates/tranquil-pds/src/sync/util.rs @@ -245,8 +245,8 @@ fn format_identity_event(event: &SequencedEvent) -> Result, SyncFrameErr FrameType::Identity, &IdentityFrame { did: event.did.clone(), - handle: event.handle.as_ref().map(|h| h.to_string()), - seq: event.seq.as_i64(), + handle: event.handle.clone(), + seq: event.seq, time: format_atproto_time(event.created_at), }, 256, diff --git a/crates/tranquil-store/src/metastore/client.rs b/crates/tranquil-store/src/metastore/client.rs index 0ee440d..b254088 100644 --- a/crates/tranquil-store/src/metastore/client.rs +++ b/crates/tranquil-store/src/metastore/client.rs @@ -3858,14 +3858,14 @@ impl tranquil_db_traits::UserRepository for MetastoreCli &self, telegram_username: &str, chat_id: i64, - handle: Option<&str>, + handle: Option<&Handle>, ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool .send(MetastoreRequest::User(UserRequest::StoreTelegramChatId { telegram_username: telegram_username.to_owned(), chat_id, - handle: handle.map(str::to_owned), + handle: handle.map(|h| h.to_string()), tx, }))?; recv(rx).await @@ -3900,14 +3900,14 @@ impl tranquil_db_traits::UserRepository for MetastoreCli &self, discord_username: &str, discord_id: &str, - handle: Option<&str>, + handle: Option<&Handle>, ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool .send(MetastoreRequest::User(UserRequest::StoreDiscordUserId { discord_username: discord_username.to_owned(), discord_id: discord_id.to_owned(), - handle: handle.map(str::to_owned), + handle: handle.map(|h| h.to_string()), tx, }))?; recv(rx).await @@ -4473,13 +4473,13 @@ impl tranquil_db_traits::UserRepository for MetastoreCli async fn get_id_by_email_or_handle( &self, email: &str, - handle: &str, + handle: &Handle, ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool .send(MetastoreRequest::User(UserRequest::GetIdByEmailOrHandle { email: email.to_owned(), - handle: handle.to_owned(), + handle: handle.to_string(), tx, }))?; recv(rx).await diff --git a/crates/tranquil-types/src/lib.rs b/crates/tranquil-types/src/lib.rs index 320ed63..666489c 100644 --- a/crates/tranquil-types/src/lib.rs +++ b/crates/tranquil-types/src/lib.rs @@ -873,7 +873,7 @@ pub mod did_doc { }) } - pub fn extract_handle(doc: &serde_json::Value) -> Option { + pub fn extract_handle(doc: &serde_json::Value) -> Option { doc.get("alsoKnownAs") .and_then(|a| a.as_array()) .and_then(|aliases| { @@ -881,7 +881,7 @@ pub mod did_doc { alias .as_str() .and_then(|s| s.strip_prefix("at://")) - .map(|h| h.to_string()) + .and_then(|h| crate::Handle::new(h).ok()) }) }) }