diff --git a/.sqlx/query-b230a27fce54d4f79de4ffcc754bf6c7e5a889623e64cdd7aaec791b3553ed83.json b/.sqlx/query-55b5e5cb13e09c4261e0452ef7e21240b504096666692c8fbd5c7dcd58607bc7.json similarity index 65% rename from .sqlx/query-b230a27fce54d4f79de4ffcc754bf6c7e5a889623e64cdd7aaec791b3553ed83.json rename to .sqlx/query-55b5e5cb13e09c4261e0452ef7e21240b504096666692c8fbd5c7dcd58607bc7.json index 8098c76..574f0fa 100644 --- a/.sqlx/query-b230a27fce54d4f79de4ffcc754bf6c7e5a889623e64cdd7aaec791b3553ed83.json +++ b/.sqlx/query-55b5e5cb13e09c4261e0452ef7e21240b504096666692c8fbd5c7dcd58607bc7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, did, email, email_verified, handle\n FROM users\n WHERE LOWER(email) = $1", + "query": "SELECT id, did, email, email_verified\n FROM users\n WHERE LOWER(email) = $1", "describe": { "columns": [ { @@ -22,11 +22,6 @@ "ordinal": 3, "name": "email_verified", "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "handle", - "type_info": "Text" } ], "parameters": { @@ -38,9 +33,8 @@ false, false, true, - false, false ] }, - "hash": "b230a27fce54d4f79de4ffcc754bf6c7e5a889623e64cdd7aaec791b3553ed83" + "hash": "55b5e5cb13e09c4261e0452ef7e21240b504096666692c8fbd5c7dcd58607bc7" } diff --git a/crates/tranquil-api/src/identity/account.rs b/crates/tranquil-api/src/identity/account.rs index 2eb9d70..2b2a14a 100644 --- a/crates/tranquil-api/src/identity/account.rs +++ b/crates/tranquil-api/src/identity/account.rs @@ -65,7 +65,7 @@ async fn try_reactivate_migration( .await { Ok(reactivated) => { - info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration"); + info!(did = %did, old_handle = ?reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration"); let secret_key_bytes = match state .repos .user @@ -202,7 +202,8 @@ pub async fn create_account( let token = extracted.token; if is_service_token(&token) { let verifier = ServiceTokenVerifier::new(); - let create_account_lxm = Nsid::from("com.atproto.server.createAccount".to_string()); + let create_account_lxm = Nsid::new("com.atproto.server.createAccount") + .expect("com.atproto.server.createAccount is a valid NSID"); match verifier .verify_service_token(&token, Some(&create_account_lxm)) .await @@ -448,7 +449,7 @@ pub async fn create_account( Ok(r) => r, Err(e) => return e.into_response(), }; - let commit_cid = CidLink::from(repo.commit_cid.to_string()); + let commit_cid = CidLink::from(&repo.commit_cid); let repo_rev = repo.repo_rev.clone(); let birthdate_pref = if tranquil_config::get().server.age_assurance_override { diff --git a/crates/tranquil-api/src/notification_prefs.rs b/crates/tranquil-api/src/notification_prefs.rs index a8408a9..dec6d39 100644 --- a/crates/tranquil-api/src/notification_prefs.rs +++ b/crates/tranquil-api/src/notification_prefs.rs @@ -147,12 +147,14 @@ pub async fn request_channel_verification( match channel { CommsChannel::Email => { let hostname = &tranquil_config::get().server.hostname; - let fallback_handle = Handle::from("user".to_string()); + let handle = handle.ok_or_else(|| { + ApiError::InternalError(Some("Email verification requires a handle".into())) + })?; tranquil_pds::comms::comms_repo::enqueue_email_update( state.repos.infra.as_ref(), user_id, identifier, - handle.unwrap_or(&fallback_handle), + handle, &formatted_token, hostname, ) diff --git a/crates/tranquil-db-traits/src/delegation.rs b/crates/tranquil-db-traits/src/delegation.rs index 1990292..0a6c841 100644 --- a/crates/tranquil-db-traits/src/delegation.rs +++ b/crates/tranquil-db-traits/src/delegation.rs @@ -23,7 +23,8 @@ pub struct DelegationGrant { #[serde(rename_all = "camelCase")] pub struct DelegatedAccountInfo { pub did: Did, - pub handle: Handle, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub handle: Option, pub granted_scopes: DbScope, pub granted_at: DateTime, } @@ -32,6 +33,7 @@ pub struct DelegatedAccountInfo { #[serde(rename_all = "camelCase")] pub struct ControllerInfo { pub did: Did, + #[serde(default, skip_serializing_if = "Option::is_none")] pub handle: Option, pub granted_scopes: DbScope, pub granted_at: DateTime, diff --git a/crates/tranquil-db-traits/src/user.rs b/crates/tranquil-db-traits/src/user.rs index 3ef0ec8..1aa7986 100644 --- a/crates/tranquil-db-traits/src/user.rs +++ b/crates/tranquil-db-traits/src/user.rs @@ -670,7 +670,6 @@ pub struct UserForVerification { pub did: Did, pub email: Option, pub email_verified: bool, - pub handle: Handle, } #[derive(Debug, Clone)] @@ -1119,7 +1118,7 @@ pub struct MigrationReactivationInput { #[derive(Debug, Clone)] pub struct ReactivatedAccountInfo { pub user_id: Uuid, - pub old_handle: Handle, + pub old_handle: Option, } #[derive(Debug, Clone)] diff --git a/crates/tranquil-db/src/postgres/delegation.rs b/crates/tranquil-db/src/postgres/delegation.rs index bd2fe3b..ef4788d 100644 --- a/crates/tranquil-db/src/postgres/delegation.rs +++ b/crates/tranquil-db/src/postgres/delegation.rs @@ -7,7 +7,9 @@ use tranquil_db_traits::{ use tranquil_types::Did; use uuid::Uuid; +use super::col; use super::user::map_sqlx_error; +use super::{column, legacy_column, opt_column}; #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)] #[sqlx(type_name = "delegation_action_type", rename_all = "snake_case")] @@ -166,16 +168,19 @@ impl DelegationRepository for PostgresDelegationRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| DelegationGrant { - id: r.id, - delegated_did: r.delegated_did.into(), - controller_did: r.controller_did.into(), - granted_scopes: DbScope::from_db(r.granted_scopes), - granted_at: r.granted_at, - granted_by: r.granted_by.into(), - revoked_at: r.revoked_at, - revoked_by: r.revoked_by.map(Into::into), - })) + row.map(|r| { + Ok(DelegationGrant { + id: r.id, + delegated_did: column(r.delegated_did, col::ACCOUNT_DELEGATIONS_DELEGATED_DID)?, + controller_did: column(r.controller_did, col::ACCOUNT_DELEGATIONS_CONTROLLER_DID)?, + granted_scopes: DbScope::from_db(r.granted_scopes), + granted_at: r.granted_at, + granted_by: column(r.granted_by, col::ACCOUNT_DELEGATIONS_GRANTED_BY)?, + revoked_at: r.revoked_at, + revoked_by: opt_column(r.revoked_by, col::ACCOUNT_DELEGATIONS_REVOKED_BY)?, + }) + }) + .transpose() } async fn get_delegations_for_account( @@ -205,17 +210,18 @@ impl DelegationRepository for PostgresDelegationRepository { .await .map_err(map_sqlx_error)?; - Ok(rows - .into_iter() - .map(|r| ControllerInfo { - did: r.controller_did.into(), - handle: r.handle.map(Into::into), - granted_scopes: DbScope::from_db(r.granted_scopes), - granted_at: r.granted_at, - is_active: r.is_active, - is_local: r.is_local, + rows.into_iter() + .map(|r| { + Ok(ControllerInfo { + did: column(r.controller_did, col::ACCOUNT_DELEGATIONS_CONTROLLER_DID)?, + handle: r.handle.and_then(|h| legacy_column(h, col::USERS_HANDLE)), + granted_scopes: DbScope::from_db(r.granted_scopes), + granted_at: r.granted_at, + is_active: r.is_active, + is_local: r.is_local, + }) }) - .collect()) + .collect() } async fn get_accounts_controlled_by( @@ -243,15 +249,16 @@ impl DelegationRepository for PostgresDelegationRepository { .await .map_err(map_sqlx_error)?; - Ok(rows - .into_iter() - .map(|r| DelegatedAccountInfo { - did: r.did.into(), - handle: r.handle.into(), - granted_scopes: DbScope::from_db(r.granted_scopes), - granted_at: r.granted_at, + rows.into_iter() + .map(|r| { + Ok(DelegatedAccountInfo { + did: column(r.did, col::USERS_DID)?, + handle: legacy_column(r.handle, col::USERS_HANDLE), + granted_scopes: DbScope::from_db(r.granted_scopes), + granted_at: r.granted_at, + }) }) - .collect()) + .collect() } async fn count_active_controllers(&self, delegated_did: &Did) -> Result { @@ -353,20 +360,27 @@ impl DelegationRepository for PostgresDelegationRepository { .await .map_err(map_sqlx_error)?; - Ok(rows - .into_iter() - .map(|r| AuditLogEntry { - id: r.id, - delegated_did: r.delegated_did.into(), - actor_did: r.actor_did.into(), - controller_did: r.controller_did.map(Into::into), - action_type: r.action_type.into(), - action_details: r.action_details, - ip_address: r.ip_address, - user_agent: r.user_agent, - created_at: r.created_at, + rows.into_iter() + .map(|r| { + Ok(AuditLogEntry { + id: r.id, + delegated_did: column( + r.delegated_did, + col::DELEGATION_AUDIT_LOG_DELEGATED_DID, + )?, + actor_did: column(r.actor_did, col::DELEGATION_AUDIT_LOG_ACTOR_DID)?, + controller_did: opt_column( + r.controller_did, + col::DELEGATION_AUDIT_LOG_CONTROLLER_DID, + )?, + action_type: r.action_type.into(), + action_details: r.action_details, + ip_address: r.ip_address, + user_agent: r.user_agent, + created_at: r.created_at, + }) }) - .collect()) + .collect() } async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result { diff --git a/crates/tranquil-db/src/postgres/user.rs b/crates/tranquil-db/src/postgres/user.rs index ab8999c..bedb465 100644 --- a/crates/tranquil-db/src/postgres/user.rs +++ b/crates/tranquil-db/src/postgres/user.rs @@ -4,6 +4,8 @@ use sqlx::PgPool; use tranquil_types::{AtIdentifier, Did, Handle, Jti, PasswordHash, TokenId}; use uuid::Uuid; +use super::col; +use super::{column, legacy_column, opt_column}; use tranquil_db_traits::{ AccountSearchResult, AccountType, ChannelVerificationStatus, CommsChannel, DbError, DidWebOverrides, NotificationPrefs, OAuthTokenWithUser, PasswordResetResult, SsoProviderType, @@ -87,17 +89,20 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserRow { - id: r.id, - did: Did::from(r.did), - handle: Handle::from(r.handle), - email: r.email, - created_at: r.created_at, - deactivated_at: r.deactivated_at, - takedown_ref: r.takedown_ref, - is_admin: r.is_admin, - inbound_migration: r.inbound_migration, - })) + row.map(|r| { + Ok(UserRow { + id: r.id, + did: column(r.did, col::USERS_DID)?, + handle: column(r.handle, col::USERS_HANDLE)?, + email: r.email, + created_at: r.created_at, + deactivated_at: r.deactivated_at, + takedown_ref: r.takedown_ref, + is_admin: r.is_admin, + inbound_migration: r.inbound_migration, + }) + }) + .transpose() } async fn get_by_handle(&self, handle: &Handle) -> Result, DbError> { @@ -110,17 +115,20 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserRow { - id: r.id, - did: Did::from(r.did), - handle: Handle::from(r.handle), - email: r.email, - created_at: r.created_at, - deactivated_at: r.deactivated_at, - takedown_ref: r.takedown_ref, - is_admin: r.is_admin, - inbound_migration: r.inbound_migration, - })) + row.map(|r| { + Ok(UserRow { + id: r.id, + did: column(r.did, col::USERS_DID)?, + handle: column(r.handle, col::USERS_HANDLE)?, + email: r.email, + created_at: r.created_at, + deactivated_at: r.deactivated_at, + takedown_ref: r.takedown_ref, + is_admin: r.is_admin, + inbound_migration: r.inbound_migration, + }) + }) + .transpose() } async fn get_with_key_by_did(&self, did: &Did) -> Result, DbError> { @@ -136,17 +144,20 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserWithKey { - id: r.id, - did: Did::from(r.did), - handle: Handle::from(r.handle), - email: r.email, - deactivated_at: r.deactivated_at, - takedown_ref: r.takedown_ref, - is_admin: r.is_admin, - key_bytes: r.key_bytes, - encryption_version: r.encryption_version, - })) + row.map(|r| { + Ok(UserWithKey { + id: r.id, + did: column(r.did, col::USERS_DID)?, + handle: column(r.handle, col::USERS_HANDLE)?, + email: r.email, + deactivated_at: r.deactivated_at, + takedown_ref: r.takedown_ref, + is_admin: r.is_admin, + key_bytes: r.key_bytes, + encryption_version: r.encryption_version, + }) + }) + .transpose() } async fn get_status_by_did(&self, did: &Did) -> Result, DbError> { @@ -207,15 +218,18 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| OAuthTokenWithUser { - did: Did::from(r.did), - expires_at: r.expires_at, - deactivated_at: r.deactivated_at, - takedown_ref: r.takedown_ref, - is_admin: r.is_admin, - key_bytes: r.key_bytes, - encryption_version: r.encryption_version, - })) + row.map(|r| { + Ok(OAuthTokenWithUser { + did: column(r.did, col::OAUTH_TOKEN_DID)?, + expires_at: r.expires_at, + deactivated_at: r.deactivated_at, + takedown_ref: r.takedown_ref, + is_admin: r.is_admin, + key_bytes: r.key_bytes, + encryption_version: r.encryption_version, + }) + }) + .transpose() } async fn get_user_info_by_did(&self, did: &Did) -> Result, DbError> { @@ -288,14 +302,16 @@ impl UserRepository for PostgresUserRepository { .map_err(map_sqlx_error)?; Ok(rows .into_iter() - .map(|r| AccountSearchResult { - did: Did::from(r.did), - handle: Handle::from(r.handle), - email: r.email, - created_at: r.created_at, - email_verified: r.email_verified, - deactivated_at: r.deactivated_at, - invites_disabled: r.invites_disabled, + .filter_map(|r| { + Some(AccountSearchResult { + did: legacy_column(r.did, col::USERS_DID)?, + handle: legacy_column(r.handle, col::USERS_HANDLE)?, + email: r.email, + created_at: r.created_at, + email_verified: r.email_verified, + deactivated_at: r.deactivated_at, + invites_disabled: r.invites_disabled, + }) }) .collect()) } @@ -311,24 +327,27 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserAuthInfo { - id: r.id, - did: Did::from(r.did), - password_hash: r.password_hash.map(PasswordHash::new), - deactivated_at: r.deactivated_at, - takedown_ref: r.takedown_ref, - channel_verification: ChannelVerificationStatus::from_db_row( - r.email_verified, - r.discord_verified, - r.telegram_verified, - r.signal_verified, - ), - })) + row.map(|r| { + Ok(UserAuthInfo { + id: r.id, + did: column(r.did, col::USERS_DID)?, + password_hash: r.password_hash.map(PasswordHash::new), + deactivated_at: r.deactivated_at, + takedown_ref: r.takedown_ref, + channel_verification: ChannelVerificationStatus::from_db_row( + r.email_verified, + r.discord_verified, + r.telegram_verified, + r.signal_verified, + ), + }) + }) + .transpose() } async fn get_by_email(&self, email: &str) -> Result, DbError> { let row = sqlx::query!( - r#"SELECT id, did, email, email_verified, handle + r#"SELECT id, did, email, email_verified FROM users WHERE LOWER(email) = $1"#, email @@ -336,13 +355,15 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserForVerification { - id: r.id, - did: Did::from(r.did), - email: r.email, - email_verified: r.email_verified, - handle: Handle::from(r.handle), - })) + row.map(|r| { + Ok(UserForVerification { + id: r.id, + did: column(r.did, col::USERS_DID)?, + email: r.email, + email_verified: r.email_verified, + }) + }) + .transpose() } async fn get_comms_prefs(&self, user_id: Uuid) -> Result, DbError> { @@ -354,15 +375,18 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserCommsPrefs { - email: r.email, - handle: Handle::from(r.handle), - preferred_channel: r.preferred_channel, - preferred_locale: r.preferred_locale, - telegram_chat_id: r.telegram_chat_id, - discord_id: r.discord_id, - signal_username: r.signal_username, - })) + row.map(|r| { + Ok(UserCommsPrefs { + email: r.email, + handle: column(r.handle, col::USERS_HANDLE)?, + preferred_channel: r.preferred_channel, + preferred_locale: r.preferred_locale, + telegram_chat_id: r.telegram_chat_id, + discord_id: r.discord_id, + signal_username: r.signal_username, + }) + }) + .transpose() } async fn get_id_by_did(&self, did: &Did) -> Result, DbError> { @@ -395,10 +419,13 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserIdAndHandle { - id: r.id, - handle: Handle::from(r.handle), - })) + row.map(|r| { + Ok(UserIdAndHandle { + id: r.id, + handle: column(r.handle, col::USERS_HANDLE)?, + }) + }) + .transpose() } async fn get_did_web_info_by_handle( @@ -412,11 +439,14 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserDidWebInfo { - id: r.id, - did: Did::from(r.did), - migrated_to_pds: r.migrated_to_pds, - })) + row.map(|r| { + Ok(UserDidWebInfo { + id: r.id, + did: column(r.did, col::USERS_DID)?, + migrated_to_pds: r.migrated_to_pds, + }) + }) + .transpose() } async fn get_did_web_overrides( @@ -441,7 +471,7 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(handle.map(Handle::from)) + opt_column(handle, col::USERS_HANDLE) } async fn check_handle_exists( @@ -538,12 +568,15 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserEmailInfo { - id: r.id, - handle: Handle::from(r.handle), - email: r.email, - email_verified: r.email_verified, - })) + row.map(|r| { + Ok(UserEmailInfo { + id: r.id, + handle: column(r.handle, col::USERS_HANDLE)?, + email: r.email, + email_verified: r.email_verified, + }) + }) + .transpose() } async fn check_email_exists( @@ -726,11 +759,14 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserIdHandleEmail { - id: r.id, - handle: Handle::from(r.handle), - email: r.email, - })) + row.map(|r| { + Ok(UserIdHandleEmail { + id: r.id, + handle: column(r.handle, col::USERS_HANDLE)?, + email: r.email, + }) + }) + .transpose() } async fn update_preferred_comms_channel( @@ -794,17 +830,20 @@ impl UserRepository for PostgresUserRepository { .fetch_optional(&self.pool) .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserVerificationInfo { - id: r.id, - handle: Handle::from(r.handle), - email: r.email, - channel_verification: ChannelVerificationStatus::from_db_row( - r.email_verified, - r.discord_verified, - r.telegram_verified, - r.signal_verified, - ), - })) + row.map(|r| { + Ok(UserVerificationInfo { + id: r.id, + handle: column(r.handle, col::USERS_HANDLE)?, + email: r.email, + channel_verification: ChannelVerificationStatus::from_db_row( + r.email_verified, + r.discord_verified, + r.telegram_verified, + r.signal_verified, + ), + }) + }) + .transpose() } async fn verify_email_channel(&self, user_id: Uuid, email: &str) -> Result { @@ -954,21 +993,22 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(rows - .into_iter() - .map(|r| StoredPasskey { - id: r.id, - did: Did::from(r.did), - credential_id: r.credential_id, - public_key: r.public_key, - sign_count: r.sign_count, - created_at: r.created_at, - last_used: r.last_used, - friendly_name: r.friendly_name, - aaguid: r.aaguid, - transports: r.transports, + rows.into_iter() + .map(|r| { + Ok(StoredPasskey { + id: r.id, + did: column(r.did, col::PASSKEYS_DID)?, + credential_id: r.credential_id, + public_key: r.public_key, + sign_count: r.sign_count, + created_at: r.created_at, + last_used: r.last_used, + friendly_name: r.friendly_name, + aaguid: r.aaguid, + transports: r.transports, + }) }) - .collect()) + .collect() } async fn get_passkey_by_credential_id( @@ -985,18 +1025,21 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| StoredPasskey { - id: r.id, - did: Did::from(r.did), - credential_id: r.credential_id, - public_key: r.public_key, - sign_count: r.sign_count, - created_at: r.created_at, - last_used: r.last_used, - friendly_name: r.friendly_name, - aaguid: r.aaguid, - transports: r.transports, - })) + row.map(|r| { + Ok(StoredPasskey { + id: r.id, + did: column(r.did, col::PASSKEYS_DID)?, + credential_id: r.credential_id, + public_key: r.public_key, + sign_count: r.sign_count, + created_at: r.created_at, + last_used: r.last_used, + friendly_name: r.friendly_name, + aaguid: r.aaguid, + transports: r.transports, + }) + }) + .transpose() } async fn save_passkey( @@ -1431,13 +1474,14 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|r| UserLoginCheck { - did: Did::from(r.did), + .map_err(map_sqlx_error)? + .map(|r| { + Ok(UserLoginCheck { + did: column(r.did, col::USERS_DID)?, password_hash: r.password_hash.map(PasswordHash::new), }) }) + .transpose() } async fn get_login_info_by_identifier( @@ -1458,11 +1502,11 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|row| UserLoginInfo { + .map_err(map_sqlx_error)? + .map(|row| { + Ok(UserLoginInfo { id: row.id, - did: Did::from(row.did), + did: column(row.did, col::USERS_DID)?, email: row.email, password_hash: row.password_hash.map(PasswordHash::new), password_required: row.password_required, @@ -1479,6 +1523,7 @@ impl UserRepository for PostgresUserRepository { account_type: row.account_type, }) }) + .transpose() } async fn get_2fa_status_by_did(&self, did: &Did) -> Result, DbError> { @@ -1527,10 +1572,10 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|row| UserSessionInfo { - handle: Handle::from(row.handle), + .map_err(map_sqlx_error)? + .map(|row| { + Ok(UserSessionInfo { + handle: column(row.handle, col::USERS_HANDLE)?, email: row.email, is_admin: row.is_admin, deactivated_at: row.deactivated_at, @@ -1549,6 +1594,7 @@ impl UserRepository for PostgresUserRepository { email_2fa_enabled: row.email_2fa_enabled, }) }) + .transpose() } async fn get_legacy_login_pref( @@ -1620,12 +1666,12 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|row| UserLoginFull { + .map_err(map_sqlx_error)? + .map(|row| { + Ok(UserLoginFull { id: row.id, - did: Did::from(row.did), - handle: Handle::from(row.handle), + did: column(row.did, col::USERS_DID)?, + handle: column(row.handle, col::USERS_HANDLE)?, password_hash: row.password_hash.map(PasswordHash::new), email: row.email, deactivated_at: row.deactivated_at, @@ -1645,6 +1691,7 @@ impl UserRepository for PostgresUserRepository { email_2fa_enabled: row.email_2fa_enabled, }) }) + .transpose() } async fn get_confirm_signup_by_did( @@ -1664,12 +1711,12 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|row| UserConfirmSignup { + .map_err(map_sqlx_error)? + .map(|row| { + Ok(UserConfirmSignup { id: row.id, - did: Did::from(row.did), - handle: Handle::from(row.handle), + did: column(row.did, col::USERS_DID)?, + handle: column(row.handle, col::USERS_HANDLE)?, email: row.email, channel: row.channel, discord_username: row.discord_username, @@ -1679,6 +1726,7 @@ impl UserRepository for PostgresUserRepository { encryption_version: row.encryption_version, }) }) + .transpose() } async fn get_resend_verification_by_did( @@ -1697,11 +1745,11 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|row| UserResendVerification { + .map_err(map_sqlx_error)? + .map(|row| { + Ok(UserResendVerification { id: row.id, - handle: Handle::from(row.handle), + handle: column(row.handle, col::USERS_HANDLE)?, email: row.email, channel: row.channel, discord_username: row.discord_username, @@ -1715,6 +1763,7 @@ impl UserRepository for PostgresUserRepository { ), }) }) + .transpose() } async fn set_channel_verified(&self, did: &Did, channel: CommsChannel) -> Result<(), DbError> { @@ -1760,14 +1809,18 @@ impl UserRepository for PostgresUserRepository { } async fn get_handles_by_email(&self, email: &str) -> Result, DbError> { - sqlx::query_scalar!( + let handles = sqlx::query_scalar!( "SELECT handle FROM users WHERE LOWER(email) = LOWER($1) AND deactivated_at IS NULL ORDER BY created_at DESC", email ) .fetch_all(&self.pool) .await - .map(|handles| handles.into_iter().map(Handle::from).collect()) - .map_err(map_sqlx_error) + .map_err(map_sqlx_error)?; + + Ok(handles + .into_iter() + .filter_map(|h| legacy_column(h, col::USERS_HANDLE)) + .collect()) } async fn set_password_reset_code( @@ -1798,15 +1851,16 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|row| UserResetCodeInfo { + .map_err(map_sqlx_error)? + .map(|row| { + Ok(UserResetCodeInfo { id: row.id, - did: Did::from(row.did), + did: column(row.did, col::USERS_DID)?, preferred_comms_channel: row.preferred_comms_channel, expires_at: row.password_reset_code_expires_at, }) }) + .transpose() } async fn clear_password_reset_code(&self, user_id: Uuid) -> Result<(), DbError> { @@ -1894,12 +1948,11 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; + let did = column(user_did, col::USERS_DID)?; + tx.commit().await.map_err(map_sqlx_error)?; - Ok(PasswordResetResult { - did: Did::from(user_did), - session_jtis, - }) + Ok(PasswordResetResult { did, session_jtis }) } async fn activate_account(&self, did: &Did) -> Result { @@ -2004,14 +2057,15 @@ impl UserRepository for PostgresUserRepository { ) .fetch_optional(&self.pool) .await - .map_err(map_sqlx_error) - .map(|opt| { - opt.map(|row| UserForDeletion { + .map_err(map_sqlx_error)? + .map(|row| { + Ok(UserForDeletion { id: row.id, password_hash: row.password_hash.map(PasswordHash::new), - handle: Handle::from(row.handle), + handle: column(row.handle, col::USERS_HANDLE)?, }) }) + .transpose() } async fn get_user_key_by_did(&self, did: &Did) -> Result, DbError> { @@ -2154,11 +2208,14 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserForDidDoc { - id: r.id, - handle: Handle::from(r.handle), - deactivated_at: r.deactivated_at, - })) + row.map(|r| { + Ok(UserForDidDoc { + id: r.id, + handle: column(r.handle, col::USERS_HANDLE)?, + deactivated_at: r.deactivated_at, + }) + }) + .transpose() } async fn get_user_for_did_doc_build( @@ -2173,11 +2230,14 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserForDidDocBuild { - id: r.id, - handle: Handle::from(r.handle), - migrated_to_pds: r.migrated_to_pds, - })) + row.map(|r| { + Ok(UserForDidDocBuild { + id: r.id, + handle: column(r.handle, col::USERS_HANDLE)?, + migrated_to_pds: r.migrated_to_pds, + }) + }) + .transpose() } async fn upsert_did_web_overrides( @@ -2234,13 +2294,16 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserForPasskeySetup { - id: r.id, - handle: Handle::from(r.handle), - recovery_token: r.recovery_token, - recovery_token_expires_at: r.recovery_token_expires_at, - password_required: r.password_required, - })) + row.map(|r| { + Ok(UserForPasskeySetup { + id: r.id, + handle: column(r.handle, col::USERS_HANDLE)?, + recovery_token: r.recovery_token, + recovery_token_expires_at: r.recovery_token_expires_at, + password_required: r.password_required, + }) + }) + .transpose() } async fn get_user_for_passkey_recovery( @@ -2257,12 +2320,15 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserForPasskeyRecovery { - id: r.id, - did: Did::from(r.did), - handle: Handle::from(r.handle), - password_required: r.password_required, - })) + row.map(|r| { + Ok(UserForPasskeyRecovery { + id: r.id, + did: column(r.did, col::USERS_DID)?, + handle: column(r.handle, col::USERS_HANDLE)?, + password_required: r.password_required, + }) + }) + .transpose() } async fn set_recovery_token( @@ -2292,13 +2358,16 @@ impl UserRepository for PostgresUserRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| UserForRecovery { - id: r.id, - did: Did::from(r.did), - preferred_comms_channel: r.preferred_comms_channel, - recovery_token: r.recovery_token, - recovery_token_expires_at: r.recovery_token_expires_at, - })) + row.map(|r| { + Ok(UserForRecovery { + id: r.id, + did: column(r.did, col::USERS_DID)?, + preferred_comms_channel: r.preferred_comms_channel, + recovery_token: r.recovery_token, + recovery_token_expires_at: r.recovery_token_expires_at, + }) + }) + .transpose() } async fn get_accounts_scheduled_for_deletion( @@ -2322,10 +2391,12 @@ impl UserRepository for PostgresUserRepository { Ok(rows .into_iter() - .map(|r| tranquil_db_traits::ScheduledDeletionAccount { - id: r.id, - did: Did::from(r.did), - handle: Handle::from(r.handle), + .filter_map(|r| { + Some(tranquil_db_traits::ScheduledDeletionAccount { + id: r.id, + did: legacy_column(r.did, col::USERS_DID)?, + handle: legacy_column(r.handle, col::USERS_HANDLE)?, + }) }) .collect()) } @@ -3029,13 +3100,15 @@ impl UserRepository for PostgresUserRepository { )); } + let old_handle = legacy_column(old_handle, col::USERS_HANDLE); + tx.commit() .await .map_err(|e| tranquil_db_traits::MigrationReactivationError::Database(e.to_string()))?; Ok(tranquil_db_traits::ReactivatedAccountInfo { user_id: account_id, - old_handle: Handle::from(old_handle), + old_handle, }) } diff --git a/crates/tranquil-store/src/metastore/delegation_ops.rs b/crates/tranquil-store/src/metastore/delegation_ops.rs index 5a5ef96..77eb2ef 100644 --- a/crates/tranquil-store/src/metastore/delegation_ops.rs +++ b/crates/tranquil-store/src/metastore/delegation_ops.rs @@ -306,9 +306,7 @@ impl DelegationOps { if let Some(val) = grant_val.filter(|v| v.revoked_at_ms.is_none()) { let delegated_did = Did::new(val.delegated_did.clone()) .map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?; - let handle = self - .resolve_handle_for_did(&val.delegated_did) - .unwrap_or_else(|| Handle::new("unknown.invalid").unwrap()); + let handle = self.resolve_handle_for_did(&val.delegated_did); acc.push(DelegatedAccountInfo { did: delegated_did, handle, diff --git a/crates/tranquil-store/src/metastore/user_ops.rs b/crates/tranquil-store/src/metastore/user_ops.rs index 17cbb97..7ca6b03 100644 --- a/crates/tranquil-store/src/metastore/user_ops.rs +++ b/crates/tranquil-store/src/metastore/user_ops.rs @@ -460,8 +460,6 @@ impl UserOps { .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, email: v.email.clone(), email_verified: v.email_verified, - handle: Handle::new(v.handle.clone()) - .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, }) }) .transpose() @@ -3000,7 +2998,13 @@ impl UserOps { } let old_handle = Handle::new(user.handle.clone()) - .map_err(|_| MigrationReactivationError::Database("invalid handle".to_owned()))?; + .inspect_err(|_| { + tracing::warn!( + handle = %user.handle, + "ignoring a stored handle that isn't valid" + ); + }) + .ok(); let user_id = user.id; let mut batch = self.db.batch(); diff --git a/frontend/src/components/dashboard/ControllersContent.svelte b/frontend/src/components/dashboard/ControllersContent.svelte index b1e1c26..1120ae9 100644 --- a/frontend/src/components/dashboard/ControllersContent.svelte +++ b/frontend/src/components/dashboard/ControllersContent.svelte @@ -26,7 +26,7 @@ interface ControlledAccount { did: Did - handle: Handle + handle?: Handle grantedScopes: ScopeSet grantedAt: string } @@ -505,7 +505,7 @@
- @{account.handle} + {account.handle ? `@${account.handle}` : account.did} {getScopeLabel(account.grantedScopes)}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 73de588..6ef4eb9 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -355,7 +355,7 @@ function _castDelegationControlledAccount( const a = raw as Record; return { did: unsafeAsDid(a.did as string), - handle: unsafeAsHandle(a.handle as string), + handle: a.handle ? unsafeAsHandle(a.handle as string) : undefined, grantedScopes: unsafeAsScopeSet( (a.granted_scopes ?? a.grantedScopes) as string, ), diff --git a/frontend/src/lib/types/api.ts b/frontend/src/lib/types/api.ts index 9c5f17c..103394a 100644 --- a/frontend/src/lib/types/api.ts +++ b/frontend/src/lib/types/api.ts @@ -529,7 +529,7 @@ export interface DelegationController { export interface DelegationControlledAccount { did: Did; - handle: Handle; + handle?: Handle; grantedScopes: ScopeSet; grantedAt: ISODateString; } diff --git a/frontend/src/routes/ActAs.svelte b/frontend/src/routes/ActAs.svelte index 5663498..fe0e6c5 100644 --- a/frontend/src/routes/ActAs.svelte +++ b/frontend/src/routes/ActAs.svelte @@ -58,7 +58,7 @@ state: state, code_challenge: codeChallenge, code_challenge_method: 'S256', - login_hint: account.handle + login_hint: account.handle ?? account.did }) })