identity: Handle type for stored handles & extract_handle

Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
Lewis
2026-07-12 08:03:14 +02:00
parent d238affd76
commit 6ca6c45605
26 changed files with 152 additions and 161 deletions
@@ -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,
+6 -9
View File
@@ -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<Handle> = 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()
+2 -1
View File
@@ -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)) => {
+13 -31
View File
@@ -48,22 +48,14 @@ pub struct CreateAccountOutput {
async fn try_reactivate_migration(
state: &AppState,
did: &str,
handle: &str,
handle: &Handle,
email: &Option<String>,
verification_channel: tranquil_db_traits::CommsChannel,
verification_recipient: Option<&str>,
) -> Option<Response> {
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,
+22 -19
View File
@@ -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<AppState>, Path(handle): Path<String>) -> 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 {}))
+2 -3
View File
@@ -26,10 +26,9 @@ pub async fn verify_handle_ownership(
_rate_limit: RateLimited<HandleVerificationLimit>,
Json(input): Json<VerifyHandleOwnershipInput>,
) -> 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()),
@@ -14,7 +14,7 @@ pub struct PlcDidResult {
pub signing_key: SigningKey,
}
pub async fn create_plc_did(state: &AppState, handle: &str) -> Result<PlcDidResult, ApiError> {
pub async fn create_plc_did(state: &AppState, handle: &Handle) -> Result<PlcDidResult, ApiError> {
use k256::SecretKey;
use rand::rngs::OsRng;
@@ -25,10 +25,7 @@ pub async fn create_plc_did(state: &AppState, handle: &str) -> Result<PlcDidResu
ApiError::InternalError(None)
})?;
let did_str = submit_plc_genesis(state, &signing_key, handle).await?;
let did: Did = did_str
.parse()
.map_err(|_| ApiError::InternalError(Some("PLC genesis returned invalid DID".into())))?;
let did = submit_plc_genesis(state, &signing_key, handle).await?;
Ok(PlcDidResult {
did,
@@ -40,7 +37,7 @@ pub async fn create_plc_did(state: &AppState, handle: &str) -> Result<PlcDidResu
pub async fn submit_plc_genesis(
state: &AppState,
signing_key: &SigningKey,
handle: &str,
handle: &Handle,
) -> Result<String, ApiError> {
let hostname = &tranquil_config::get().server.hostname;
let pds_endpoint = format!("https://{}", hostname);
@@ -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<String, ApiError> {
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,
)
@@ -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: {}",
@@ -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,
+5 -2
View File
@@ -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,
+11 -1
View File
@@ -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
+3 -2
View File
@@ -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)) => {
+3 -3
View File
@@ -264,7 +264,7 @@ pub trait UserRepository: Send + Sync {
&self,
telegram_username: &str,
chat_id: i64,
handle: Option<&str>,
handle: Option<&Handle>,
) -> Result<Option<Uuid>, DbError>;
async fn get_telegram_chat_id(&self, user_id: Uuid) -> Result<Option<i64>, DbError>;
@@ -279,7 +279,7 @@ pub trait UserRepository: Send + Sync {
&self,
discord_username: &str,
discord_id: &str,
handle: Option<&str>,
handle: Option<&Handle>,
) -> Result<Option<Uuid>, 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<Option<Uuid>, DbError>;
async fn count_accounts_by_email(&self, email: &str) -> Result<i64, DbError>;
+4 -4
View File
@@ -1732,12 +1732,12 @@ impl UserRepository for PostgresUserRepository {
async fn get_id_by_email_or_handle(
&self,
email: &str,
handle: &str,
handle: &Handle,
) -> Result<Option<Uuid>, 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<Option<Uuid>, 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<Option<Uuid>, DbError> {
let result = match handle {
Some(h) => sqlx::query_scalar!(
@@ -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<String>,
@@ -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,
+5 -4
View File
@@ -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<String, HandleValidationError> {
pub fn validate_full_domain_handle(handle: &str) -> Result<Handle, HandleValidationError> {
let handle = handle.trim();
if handle.is_empty() {
@@ -189,14 +190,14 @@ pub fn validate_full_domain_handle(handle: &str) -> Result<String, HandleValidat
return Err(HandleValidationError::BannedWord);
}
Ok(handle_lower)
Ok(Handle::from(handle_lower))
}
pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> {
validate_service_handle(handle, ReservedHandlePolicy::Reject)
}
pub fn resolve_handle_input(input: &str) -> Result<String, HandleValidationError> {
pub fn resolve_handle_input(input: &str) -> Result<Handle, HandleValidationError> {
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<String, HandleValidationError
None => input,
};
let validated = validate_short_handle(handle_to_validate)?;
Ok(format!(
Ok(Handle::from(format!(
"{}.{}",
validated,
matched_domain.unwrap_or(&available_domains[0])
+1 -1
View File
@@ -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)
}
+2 -2
View File
@@ -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<Uuid, DbError> {
@@ -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),
+4 -3
View File
@@ -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<String>,
pub handle: Option<Handle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pds_url: Option<String>,
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(),
+5 -4
View File
@@ -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<String, HandleResolutionError> {
pub async fn resolve_handle_dns(handle: &Handle) -> Result<Did, HandleResolutionError> {
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<String, HandleResolution
.ok_or(HandleResolutionError::NotFound)
}
pub async fn resolve_handle_http(handle: &str) -> Result<String, HandleResolutionError> {
pub async fn resolve_handle_http(handle: &Handle) -> Result<Did, HandleResolutionError> {
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<String, HandleResolutio
}
}
pub async fn resolve_handle(handle: &str) -> Result<String, HandleResolutionError> {
pub async fn resolve_handle(handle: &Handle) -> Result<Did, HandleResolutionError> {
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<String, HandleResolutionErro
}
pub async fn verify_handle_ownership(
handle: &str,
handle: &Handle,
expected_did: &str,
) -> Result<(), HandleResolutionError> {
let resolved_did = resolve_handle(handle).await?;
+9 -4
View File
@@ -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<GenesisResult, PlcError> {
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()
+2 -2
View File
@@ -65,8 +65,8 @@ pub struct RepoOp {
pub struct IdentityFrame {
pub did: Did,
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
pub seq: i64,
pub handle: Option<Handle>,
pub seq: SequenceNumber,
pub time: String,
}
+2 -2
View File
@@ -245,8 +245,8 @@ fn format_identity_event(event: &SequencedEvent) -> Result<Vec<u8>, 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,
@@ -3858,14 +3858,14 @@ impl<S: StorageIO + 'static> tranquil_db_traits::UserRepository for MetastoreCli
&self,
telegram_username: &str,
chat_id: i64,
handle: Option<&str>,
handle: Option<&Handle>,
) -> Result<Option<Uuid>, 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<S: StorageIO + 'static> tranquil_db_traits::UserRepository for MetastoreCli
&self,
discord_username: &str,
discord_id: &str,
handle: Option<&str>,
handle: Option<&Handle>,
) -> Result<Option<Uuid>, 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<S: StorageIO + 'static> tranquil_db_traits::UserRepository for MetastoreCli
async fn get_id_by_email_or_handle(
&self,
email: &str,
handle: &str,
handle: &Handle,
) -> Result<Option<Uuid>, 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
+2 -2
View File
@@ -873,7 +873,7 @@ pub mod did_doc {
})
}
pub fn extract_handle(doc: &serde_json::Value) -> Option<String> {
pub fn extract_handle(doc: &serde_json::Value) -> Option<crate::Handle> {
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())
})
})
}