Our Personal Data Server from scratch!
0

Configure Feed

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

db: make stored handle optional when it no longer parses

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jul 24, 2026, 8:27 PM +0300) commit a08e4756 parent c5ec5dc5 change-id ywxpwvoo
+369 -282
+2 -8
.sqlx/query-b230a27fce54d4f79de4ffcc754bf6c7e5a889623e64cdd7aaec791b3553ed83.json .sqlx/query-55b5e5cb13e09c4261e0452ef7e21240b504096666692c8fbd5c7dcd58607bc7.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "SELECT id, did, email, email_verified, handle\n FROM users\n WHERE LOWER(email) = $1", 3 + "query": "SELECT id, did, email, email_verified\n FROM users\n WHERE LOWER(email) = $1", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 22 22 "ordinal": 3, 23 23 "name": "email_verified", 24 24 "type_info": "Bool" 25 - }, 26 - { 27 - "ordinal": 4, 28 - "name": "handle", 29 - "type_info": "Text" 30 25 } 31 26 ], 32 27 "parameters": { ··· 38 33 false, 39 34 false, 40 35 true, 41 - false, 42 36 false 43 37 ] 44 38 }, 45 - "hash": "b230a27fce54d4f79de4ffcc754bf6c7e5a889623e64cdd7aaec791b3553ed83" 39 + "hash": "55b5e5cb13e09c4261e0452ef7e21240b504096666692c8fbd5c7dcd58607bc7" 46 40 }
+4 -3
crates/tranquil-api/src/identity/account.rs
··· 65 65 .await 66 66 { 67 67 Ok(reactivated) => { 68 - info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration"); 68 + info!(did = %did, old_handle = ?reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration"); 69 69 let secret_key_bytes = match state 70 70 .repos 71 71 .user ··· 202 202 let token = extracted.token; 203 203 if is_service_token(&token) { 204 204 let verifier = ServiceTokenVerifier::new(); 205 - let create_account_lxm = Nsid::from("com.atproto.server.createAccount".to_string()); 205 + let create_account_lxm = Nsid::new("com.atproto.server.createAccount") 206 + .expect("com.atproto.server.createAccount is a valid NSID"); 206 207 match verifier 207 208 .verify_service_token(&token, Some(&create_account_lxm)) 208 209 .await ··· 448 449 Ok(r) => r, 449 450 Err(e) => return e.into_response(), 450 451 }; 451 - let commit_cid = CidLink::from(repo.commit_cid.to_string()); 452 + let commit_cid = CidLink::from(&repo.commit_cid); 452 453 let repo_rev = repo.repo_rev.clone(); 453 454 454 455 let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
+4 -2
crates/tranquil-api/src/notification_prefs.rs
··· 147 147 match channel { 148 148 CommsChannel::Email => { 149 149 let hostname = &tranquil_config::get().server.hostname; 150 - let fallback_handle = Handle::from("user".to_string()); 150 + let handle = handle.ok_or_else(|| { 151 + ApiError::InternalError(Some("Email verification requires a handle".into())) 152 + })?; 151 153 tranquil_pds::comms::comms_repo::enqueue_email_update( 152 154 state.repos.infra.as_ref(), 153 155 user_id, 154 156 identifier, 155 - handle.unwrap_or(&fallback_handle), 157 + handle, 156 158 &formatted_token, 157 159 hostname, 158 160 )
+3 -1
crates/tranquil-db-traits/src/delegation.rs
··· 23 23 #[serde(rename_all = "camelCase")] 24 24 pub struct DelegatedAccountInfo { 25 25 pub did: Did, 26 - pub handle: Handle, 26 + #[serde(default, skip_serializing_if = "Option::is_none")] 27 + pub handle: Option<Handle>, 27 28 pub granted_scopes: DbScope, 28 29 pub granted_at: DateTime<Utc>, 29 30 } ··· 32 33 #[serde(rename_all = "camelCase")] 33 34 pub struct ControllerInfo { 34 35 pub did: Did, 36 + #[serde(default, skip_serializing_if = "Option::is_none")] 35 37 pub handle: Option<Handle>, 36 38 pub granted_scopes: DbScope, 37 39 pub granted_at: DateTime<Utc>,
+1 -2
crates/tranquil-db-traits/src/user.rs
··· 670 670 pub did: Did, 671 671 pub email: Option<String>, 672 672 pub email_verified: bool, 673 - pub handle: Handle, 674 673 } 675 674 676 675 #[derive(Debug, Clone)] ··· 1119 1118 #[derive(Debug, Clone)] 1120 1119 pub struct ReactivatedAccountInfo { 1121 1120 pub user_id: Uuid, 1122 - pub old_handle: Handle, 1121 + pub old_handle: Option<Handle>, 1123 1122 } 1124 1123 1125 1124 #[derive(Debug, Clone)]
+55 -41
crates/tranquil-db/src/postgres/delegation.rs
··· 7 7 use tranquil_types::Did; 8 8 use uuid::Uuid; 9 9 10 + use super::col; 10 11 use super::user::map_sqlx_error; 12 + use super::{column, legacy_column, opt_column}; 11 13 12 14 #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)] 13 15 #[sqlx(type_name = "delegation_action_type", rename_all = "snake_case")] ··· 166 168 .await 167 169 .map_err(map_sqlx_error)?; 168 170 169 - Ok(row.map(|r| DelegationGrant { 170 - id: r.id, 171 - delegated_did: r.delegated_did.into(), 172 - controller_did: r.controller_did.into(), 173 - granted_scopes: DbScope::from_db(r.granted_scopes), 174 - granted_at: r.granted_at, 175 - granted_by: r.granted_by.into(), 176 - revoked_at: r.revoked_at, 177 - revoked_by: r.revoked_by.map(Into::into), 178 - })) 171 + row.map(|r| { 172 + Ok(DelegationGrant { 173 + id: r.id, 174 + delegated_did: column(r.delegated_did, col::ACCOUNT_DELEGATIONS_DELEGATED_DID)?, 175 + controller_did: column(r.controller_did, col::ACCOUNT_DELEGATIONS_CONTROLLER_DID)?, 176 + granted_scopes: DbScope::from_db(r.granted_scopes), 177 + granted_at: r.granted_at, 178 + granted_by: column(r.granted_by, col::ACCOUNT_DELEGATIONS_GRANTED_BY)?, 179 + revoked_at: r.revoked_at, 180 + revoked_by: opt_column(r.revoked_by, col::ACCOUNT_DELEGATIONS_REVOKED_BY)?, 181 + }) 182 + }) 183 + .transpose() 179 184 } 180 185 181 186 async fn get_delegations_for_account( ··· 205 210 .await 206 211 .map_err(map_sqlx_error)?; 207 212 208 - Ok(rows 209 - .into_iter() 210 - .map(|r| ControllerInfo { 211 - did: r.controller_did.into(), 212 - handle: r.handle.map(Into::into), 213 - granted_scopes: DbScope::from_db(r.granted_scopes), 214 - granted_at: r.granted_at, 215 - is_active: r.is_active, 216 - is_local: r.is_local, 213 + rows.into_iter() 214 + .map(|r| { 215 + Ok(ControllerInfo { 216 + did: column(r.controller_did, col::ACCOUNT_DELEGATIONS_CONTROLLER_DID)?, 217 + handle: r.handle.and_then(|h| legacy_column(h, col::USERS_HANDLE)), 218 + granted_scopes: DbScope::from_db(r.granted_scopes), 219 + granted_at: r.granted_at, 220 + is_active: r.is_active, 221 + is_local: r.is_local, 222 + }) 217 223 }) 218 - .collect()) 224 + .collect() 219 225 } 220 226 221 227 async fn get_accounts_controlled_by( ··· 243 249 .await 244 250 .map_err(map_sqlx_error)?; 245 251 246 - Ok(rows 247 - .into_iter() 248 - .map(|r| DelegatedAccountInfo { 249 - did: r.did.into(), 250 - handle: r.handle.into(), 251 - granted_scopes: DbScope::from_db(r.granted_scopes), 252 - granted_at: r.granted_at, 252 + rows.into_iter() 253 + .map(|r| { 254 + Ok(DelegatedAccountInfo { 255 + did: column(r.did, col::USERS_DID)?, 256 + handle: legacy_column(r.handle, col::USERS_HANDLE), 257 + granted_scopes: DbScope::from_db(r.granted_scopes), 258 + granted_at: r.granted_at, 259 + }) 253 260 }) 254 - .collect()) 261 + .collect() 255 262 } 256 263 257 264 async fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, DbError> { ··· 353 360 .await 354 361 .map_err(map_sqlx_error)?; 355 362 356 - Ok(rows 357 - .into_iter() 358 - .map(|r| AuditLogEntry { 359 - id: r.id, 360 - delegated_did: r.delegated_did.into(), 361 - actor_did: r.actor_did.into(), 362 - controller_did: r.controller_did.map(Into::into), 363 - action_type: r.action_type.into(), 364 - action_details: r.action_details, 365 - ip_address: r.ip_address, 366 - user_agent: r.user_agent, 367 - created_at: r.created_at, 363 + rows.into_iter() 364 + .map(|r| { 365 + Ok(AuditLogEntry { 366 + id: r.id, 367 + delegated_did: column( 368 + r.delegated_did, 369 + col::DELEGATION_AUDIT_LOG_DELEGATED_DID, 370 + )?, 371 + actor_did: column(r.actor_did, col::DELEGATION_AUDIT_LOG_ACTOR_DID)?, 372 + controller_did: opt_column( 373 + r.controller_did, 374 + col::DELEGATION_AUDIT_LOG_CONTROLLER_DID, 375 + )?, 376 + action_type: r.action_type.into(), 377 + action_details: r.action_details, 378 + ip_address: r.ip_address, 379 + user_agent: r.user_agent, 380 + created_at: r.created_at, 381 + }) 368 382 }) 369 - .collect()) 383 + .collect() 370 384 } 371 385 372 386 async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, DbError> {
+287 -214
crates/tranquil-db/src/postgres/user.rs
··· 4 4 use tranquil_types::{AtIdentifier, Did, Handle, Jti, PasswordHash, TokenId}; 5 5 use uuid::Uuid; 6 6 7 + use super::col; 8 + use super::{column, legacy_column, opt_column}; 7 9 use tranquil_db_traits::{ 8 10 AccountSearchResult, AccountType, ChannelVerificationStatus, CommsChannel, DbError, 9 11 DidWebOverrides, NotificationPrefs, OAuthTokenWithUser, PasswordResetResult, SsoProviderType, ··· 87 89 .await 88 90 .map_err(map_sqlx_error)?; 89 91 90 - Ok(row.map(|r| UserRow { 91 - id: r.id, 92 - did: Did::from(r.did), 93 - handle: Handle::from(r.handle), 94 - email: r.email, 95 - created_at: r.created_at, 96 - deactivated_at: r.deactivated_at, 97 - takedown_ref: r.takedown_ref, 98 - is_admin: r.is_admin, 99 - inbound_migration: r.inbound_migration, 100 - })) 92 + row.map(|r| { 93 + Ok(UserRow { 94 + id: r.id, 95 + did: column(r.did, col::USERS_DID)?, 96 + handle: column(r.handle, col::USERS_HANDLE)?, 97 + email: r.email, 98 + created_at: r.created_at, 99 + deactivated_at: r.deactivated_at, 100 + takedown_ref: r.takedown_ref, 101 + is_admin: r.is_admin, 102 + inbound_migration: r.inbound_migration, 103 + }) 104 + }) 105 + .transpose() 101 106 } 102 107 103 108 async fn get_by_handle(&self, handle: &Handle) -> Result<Option<UserRow>, DbError> { ··· 110 115 .await 111 116 .map_err(map_sqlx_error)?; 112 117 113 - Ok(row.map(|r| UserRow { 114 - id: r.id, 115 - did: Did::from(r.did), 116 - handle: Handle::from(r.handle), 117 - email: r.email, 118 - created_at: r.created_at, 119 - deactivated_at: r.deactivated_at, 120 - takedown_ref: r.takedown_ref, 121 - is_admin: r.is_admin, 122 - inbound_migration: r.inbound_migration, 123 - })) 118 + row.map(|r| { 119 + Ok(UserRow { 120 + id: r.id, 121 + did: column(r.did, col::USERS_DID)?, 122 + handle: column(r.handle, col::USERS_HANDLE)?, 123 + email: r.email, 124 + created_at: r.created_at, 125 + deactivated_at: r.deactivated_at, 126 + takedown_ref: r.takedown_ref, 127 + is_admin: r.is_admin, 128 + inbound_migration: r.inbound_migration, 129 + }) 130 + }) 131 + .transpose() 124 132 } 125 133 126 134 async fn get_with_key_by_did(&self, did: &Did) -> Result<Option<UserWithKey>, DbError> { ··· 136 144 .await 137 145 .map_err(map_sqlx_error)?; 138 146 139 - Ok(row.map(|r| UserWithKey { 140 - id: r.id, 141 - did: Did::from(r.did), 142 - handle: Handle::from(r.handle), 143 - email: r.email, 144 - deactivated_at: r.deactivated_at, 145 - takedown_ref: r.takedown_ref, 146 - is_admin: r.is_admin, 147 - key_bytes: r.key_bytes, 148 - encryption_version: r.encryption_version, 149 - })) 147 + row.map(|r| { 148 + Ok(UserWithKey { 149 + id: r.id, 150 + did: column(r.did, col::USERS_DID)?, 151 + handle: column(r.handle, col::USERS_HANDLE)?, 152 + email: r.email, 153 + deactivated_at: r.deactivated_at, 154 + takedown_ref: r.takedown_ref, 155 + is_admin: r.is_admin, 156 + key_bytes: r.key_bytes, 157 + encryption_version: r.encryption_version, 158 + }) 159 + }) 160 + .transpose() 150 161 } 151 162 152 163 async fn get_status_by_did(&self, did: &Did) -> Result<Option<UserStatus>, DbError> { ··· 207 218 .await 208 219 .map_err(map_sqlx_error)?; 209 220 210 - Ok(row.map(|r| OAuthTokenWithUser { 211 - did: Did::from(r.did), 212 - expires_at: r.expires_at, 213 - deactivated_at: r.deactivated_at, 214 - takedown_ref: r.takedown_ref, 215 - is_admin: r.is_admin, 216 - key_bytes: r.key_bytes, 217 - encryption_version: r.encryption_version, 218 - })) 221 + row.map(|r| { 222 + Ok(OAuthTokenWithUser { 223 + did: column(r.did, col::OAUTH_TOKEN_DID)?, 224 + expires_at: r.expires_at, 225 + deactivated_at: r.deactivated_at, 226 + takedown_ref: r.takedown_ref, 227 + is_admin: r.is_admin, 228 + key_bytes: r.key_bytes, 229 + encryption_version: r.encryption_version, 230 + }) 231 + }) 232 + .transpose() 219 233 } 220 234 221 235 async fn get_user_info_by_did(&self, did: &Did) -> Result<Option<UserInfoForAuth>, DbError> { ··· 288 302 .map_err(map_sqlx_error)?; 289 303 Ok(rows 290 304 .into_iter() 291 - .map(|r| AccountSearchResult { 292 - did: Did::from(r.did), 293 - handle: Handle::from(r.handle), 294 - email: r.email, 295 - created_at: r.created_at, 296 - email_verified: r.email_verified, 297 - deactivated_at: r.deactivated_at, 298 - invites_disabled: r.invites_disabled, 305 + .filter_map(|r| { 306 + Some(AccountSearchResult { 307 + did: legacy_column(r.did, col::USERS_DID)?, 308 + handle: legacy_column(r.handle, col::USERS_HANDLE)?, 309 + email: r.email, 310 + created_at: r.created_at, 311 + email_verified: r.email_verified, 312 + deactivated_at: r.deactivated_at, 313 + invites_disabled: r.invites_disabled, 314 + }) 299 315 }) 300 316 .collect()) 301 317 } ··· 311 327 .fetch_optional(&self.pool) 312 328 .await 313 329 .map_err(map_sqlx_error)?; 314 - Ok(row.map(|r| UserAuthInfo { 315 - id: r.id, 316 - did: Did::from(r.did), 317 - password_hash: r.password_hash.map(PasswordHash::new), 318 - deactivated_at: r.deactivated_at, 319 - takedown_ref: r.takedown_ref, 320 - channel_verification: ChannelVerificationStatus::from_db_row( 321 - r.email_verified, 322 - r.discord_verified, 323 - r.telegram_verified, 324 - r.signal_verified, 325 - ), 326 - })) 330 + row.map(|r| { 331 + Ok(UserAuthInfo { 332 + id: r.id, 333 + did: column(r.did, col::USERS_DID)?, 334 + password_hash: r.password_hash.map(PasswordHash::new), 335 + deactivated_at: r.deactivated_at, 336 + takedown_ref: r.takedown_ref, 337 + channel_verification: ChannelVerificationStatus::from_db_row( 338 + r.email_verified, 339 + r.discord_verified, 340 + r.telegram_verified, 341 + r.signal_verified, 342 + ), 343 + }) 344 + }) 345 + .transpose() 327 346 } 328 347 329 348 async fn get_by_email(&self, email: &str) -> Result<Option<UserForVerification>, DbError> { 330 349 let row = sqlx::query!( 331 - r#"SELECT id, did, email, email_verified, handle 350 + r#"SELECT id, did, email, email_verified 332 351 FROM users 333 352 WHERE LOWER(email) = $1"#, 334 353 email ··· 336 355 .fetch_optional(&self.pool) 337 356 .await 338 357 .map_err(map_sqlx_error)?; 339 - Ok(row.map(|r| UserForVerification { 340 - id: r.id, 341 - did: Did::from(r.did), 342 - email: r.email, 343 - email_verified: r.email_verified, 344 - handle: Handle::from(r.handle), 345 - })) 358 + row.map(|r| { 359 + Ok(UserForVerification { 360 + id: r.id, 361 + did: column(r.did, col::USERS_DID)?, 362 + email: r.email, 363 + email_verified: r.email_verified, 364 + }) 365 + }) 366 + .transpose() 346 367 } 347 368 348 369 async fn get_comms_prefs(&self, user_id: Uuid) -> Result<Option<UserCommsPrefs>, DbError> { ··· 354 375 .fetch_optional(&self.pool) 355 376 .await 356 377 .map_err(map_sqlx_error)?; 357 - Ok(row.map(|r| UserCommsPrefs { 358 - email: r.email, 359 - handle: Handle::from(r.handle), 360 - preferred_channel: r.preferred_channel, 361 - preferred_locale: r.preferred_locale, 362 - telegram_chat_id: r.telegram_chat_id, 363 - discord_id: r.discord_id, 364 - signal_username: r.signal_username, 365 - })) 378 + row.map(|r| { 379 + Ok(UserCommsPrefs { 380 + email: r.email, 381 + handle: column(r.handle, col::USERS_HANDLE)?, 382 + preferred_channel: r.preferred_channel, 383 + preferred_locale: r.preferred_locale, 384 + telegram_chat_id: r.telegram_chat_id, 385 + discord_id: r.discord_id, 386 + signal_username: r.signal_username, 387 + }) 388 + }) 389 + .transpose() 366 390 } 367 391 368 392 async fn get_id_by_did(&self, did: &Did) -> Result<Option<Uuid>, DbError> { ··· 395 419 .fetch_optional(&self.pool) 396 420 .await 397 421 .map_err(map_sqlx_error)?; 398 - Ok(row.map(|r| UserIdAndHandle { 399 - id: r.id, 400 - handle: Handle::from(r.handle), 401 - })) 422 + row.map(|r| { 423 + Ok(UserIdAndHandle { 424 + id: r.id, 425 + handle: column(r.handle, col::USERS_HANDLE)?, 426 + }) 427 + }) 428 + .transpose() 402 429 } 403 430 404 431 async fn get_did_web_info_by_handle( ··· 412 439 .fetch_optional(&self.pool) 413 440 .await 414 441 .map_err(map_sqlx_error)?; 415 - Ok(row.map(|r| UserDidWebInfo { 416 - id: r.id, 417 - did: Did::from(r.did), 418 - migrated_to_pds: r.migrated_to_pds, 419 - })) 442 + row.map(|r| { 443 + Ok(UserDidWebInfo { 444 + id: r.id, 445 + did: column(r.did, col::USERS_DID)?, 446 + migrated_to_pds: r.migrated_to_pds, 447 + }) 448 + }) 449 + .transpose() 420 450 } 421 451 422 452 async fn get_did_web_overrides( ··· 441 471 .fetch_optional(&self.pool) 442 472 .await 443 473 .map_err(map_sqlx_error)?; 444 - Ok(handle.map(Handle::from)) 474 + opt_column(handle, col::USERS_HANDLE) 445 475 } 446 476 447 477 async fn check_handle_exists( ··· 538 568 .fetch_optional(&self.pool) 539 569 .await 540 570 .map_err(map_sqlx_error)?; 541 - Ok(row.map(|r| UserEmailInfo { 542 - id: r.id, 543 - handle: Handle::from(r.handle), 544 - email: r.email, 545 - email_verified: r.email_verified, 546 - })) 571 + row.map(|r| { 572 + Ok(UserEmailInfo { 573 + id: r.id, 574 + handle: column(r.handle, col::USERS_HANDLE)?, 575 + email: r.email, 576 + email_verified: r.email_verified, 577 + }) 578 + }) 579 + .transpose() 547 580 } 548 581 549 582 async fn check_email_exists( ··· 726 759 .fetch_optional(&self.pool) 727 760 .await 728 761 .map_err(map_sqlx_error)?; 729 - Ok(row.map(|r| UserIdHandleEmail { 730 - id: r.id, 731 - handle: Handle::from(r.handle), 732 - email: r.email, 733 - })) 762 + row.map(|r| { 763 + Ok(UserIdHandleEmail { 764 + id: r.id, 765 + handle: column(r.handle, col::USERS_HANDLE)?, 766 + email: r.email, 767 + }) 768 + }) 769 + .transpose() 734 770 } 735 771 736 772 async fn update_preferred_comms_channel( ··· 794 830 .fetch_optional(&self.pool) 795 831 .await 796 832 .map_err(map_sqlx_error)?; 797 - Ok(row.map(|r| UserVerificationInfo { 798 - id: r.id, 799 - handle: Handle::from(r.handle), 800 - email: r.email, 801 - channel_verification: ChannelVerificationStatus::from_db_row( 802 - r.email_verified, 803 - r.discord_verified, 804 - r.telegram_verified, 805 - r.signal_verified, 806 - ), 807 - })) 833 + row.map(|r| { 834 + Ok(UserVerificationInfo { 835 + id: r.id, 836 + handle: column(r.handle, col::USERS_HANDLE)?, 837 + email: r.email, 838 + channel_verification: ChannelVerificationStatus::from_db_row( 839 + r.email_verified, 840 + r.discord_verified, 841 + r.telegram_verified, 842 + r.signal_verified, 843 + ), 844 + }) 845 + }) 846 + .transpose() 808 847 } 809 848 810 849 async fn verify_email_channel(&self, user_id: Uuid, email: &str) -> Result<bool, DbError> { ··· 954 993 .await 955 994 .map_err(map_sqlx_error)?; 956 995 957 - Ok(rows 958 - .into_iter() 959 - .map(|r| StoredPasskey { 960 - id: r.id, 961 - did: Did::from(r.did), 962 - credential_id: r.credential_id, 963 - public_key: r.public_key, 964 - sign_count: r.sign_count, 965 - created_at: r.created_at, 966 - last_used: r.last_used, 967 - friendly_name: r.friendly_name, 968 - aaguid: r.aaguid, 969 - transports: r.transports, 996 + rows.into_iter() 997 + .map(|r| { 998 + Ok(StoredPasskey { 999 + id: r.id, 1000 + did: column(r.did, col::PASSKEYS_DID)?, 1001 + credential_id: r.credential_id, 1002 + public_key: r.public_key, 1003 + sign_count: r.sign_count, 1004 + created_at: r.created_at, 1005 + last_used: r.last_used, 1006 + friendly_name: r.friendly_name, 1007 + aaguid: r.aaguid, 1008 + transports: r.transports, 1009 + }) 970 1010 }) 971 - .collect()) 1011 + .collect() 972 1012 } 973 1013 974 1014 async fn get_passkey_by_credential_id( ··· 985 1025 .await 986 1026 .map_err(map_sqlx_error)?; 987 1027 988 - Ok(row.map(|r| StoredPasskey { 989 - id: r.id, 990 - did: Did::from(r.did), 991 - credential_id: r.credential_id, 992 - public_key: r.public_key, 993 - sign_count: r.sign_count, 994 - created_at: r.created_at, 995 - last_used: r.last_used, 996 - friendly_name: r.friendly_name, 997 - aaguid: r.aaguid, 998 - transports: r.transports, 999 - })) 1028 + row.map(|r| { 1029 + Ok(StoredPasskey { 1030 + id: r.id, 1031 + did: column(r.did, col::PASSKEYS_DID)?, 1032 + credential_id: r.credential_id, 1033 + public_key: r.public_key, 1034 + sign_count: r.sign_count, 1035 + created_at: r.created_at, 1036 + last_used: r.last_used, 1037 + friendly_name: r.friendly_name, 1038 + aaguid: r.aaguid, 1039 + transports: r.transports, 1040 + }) 1041 + }) 1042 + .transpose() 1000 1043 } 1001 1044 1002 1045 async fn save_passkey( ··· 1431 1474 ) 1432 1475 .fetch_optional(&self.pool) 1433 1476 .await 1434 - .map_err(map_sqlx_error) 1435 - .map(|opt| { 1436 - opt.map(|r| UserLoginCheck { 1437 - did: Did::from(r.did), 1477 + .map_err(map_sqlx_error)? 1478 + .map(|r| { 1479 + Ok(UserLoginCheck { 1480 + did: column(r.did, col::USERS_DID)?, 1438 1481 password_hash: r.password_hash.map(PasswordHash::new), 1439 1482 }) 1440 1483 }) 1484 + .transpose() 1441 1485 } 1442 1486 1443 1487 async fn get_login_info_by_identifier( ··· 1458 1502 ) 1459 1503 .fetch_optional(&self.pool) 1460 1504 .await 1461 - .map_err(map_sqlx_error) 1462 - .map(|opt| { 1463 - opt.map(|row| UserLoginInfo { 1505 + .map_err(map_sqlx_error)? 1506 + .map(|row| { 1507 + Ok(UserLoginInfo { 1464 1508 id: row.id, 1465 - did: Did::from(row.did), 1509 + did: column(row.did, col::USERS_DID)?, 1466 1510 email: row.email, 1467 1511 password_hash: row.password_hash.map(PasswordHash::new), 1468 1512 password_required: row.password_required, ··· 1479 1523 account_type: row.account_type, 1480 1524 }) 1481 1525 }) 1526 + .transpose() 1482 1527 } 1483 1528 1484 1529 async fn get_2fa_status_by_did(&self, did: &Did) -> Result<Option<User2faStatus>, DbError> { ··· 1527 1572 ) 1528 1573 .fetch_optional(&self.pool) 1529 1574 .await 1530 - .map_err(map_sqlx_error) 1531 - .map(|opt| { 1532 - opt.map(|row| UserSessionInfo { 1533 - handle: Handle::from(row.handle), 1575 + .map_err(map_sqlx_error)? 1576 + .map(|row| { 1577 + Ok(UserSessionInfo { 1578 + handle: column(row.handle, col::USERS_HANDLE)?, 1534 1579 email: row.email, 1535 1580 is_admin: row.is_admin, 1536 1581 deactivated_at: row.deactivated_at, ··· 1549 1594 email_2fa_enabled: row.email_2fa_enabled, 1550 1595 }) 1551 1596 }) 1597 + .transpose() 1552 1598 } 1553 1599 1554 1600 async fn get_legacy_login_pref( ··· 1620 1666 ) 1621 1667 .fetch_optional(&self.pool) 1622 1668 .await 1623 - .map_err(map_sqlx_error) 1624 - .map(|opt| { 1625 - opt.map(|row| UserLoginFull { 1669 + .map_err(map_sqlx_error)? 1670 + .map(|row| { 1671 + Ok(UserLoginFull { 1626 1672 id: row.id, 1627 - did: Did::from(row.did), 1628 - handle: Handle::from(row.handle), 1673 + did: column(row.did, col::USERS_DID)?, 1674 + handle: column(row.handle, col::USERS_HANDLE)?, 1629 1675 password_hash: row.password_hash.map(PasswordHash::new), 1630 1676 email: row.email, 1631 1677 deactivated_at: row.deactivated_at, ··· 1645 1691 email_2fa_enabled: row.email_2fa_enabled, 1646 1692 }) 1647 1693 }) 1694 + .transpose() 1648 1695 } 1649 1696 1650 1697 async fn get_confirm_signup_by_did( ··· 1664 1711 ) 1665 1712 .fetch_optional(&self.pool) 1666 1713 .await 1667 - .map_err(map_sqlx_error) 1668 - .map(|opt| { 1669 - opt.map(|row| UserConfirmSignup { 1714 + .map_err(map_sqlx_error)? 1715 + .map(|row| { 1716 + Ok(UserConfirmSignup { 1670 1717 id: row.id, 1671 - did: Did::from(row.did), 1672 - handle: Handle::from(row.handle), 1718 + did: column(row.did, col::USERS_DID)?, 1719 + handle: column(row.handle, col::USERS_HANDLE)?, 1673 1720 email: row.email, 1674 1721 channel: row.channel, 1675 1722 discord_username: row.discord_username, ··· 1679 1726 encryption_version: row.encryption_version, 1680 1727 }) 1681 1728 }) 1729 + .transpose() 1682 1730 } 1683 1731 1684 1732 async fn get_resend_verification_by_did( ··· 1697 1745 ) 1698 1746 .fetch_optional(&self.pool) 1699 1747 .await 1700 - .map_err(map_sqlx_error) 1701 - .map(|opt| { 1702 - opt.map(|row| UserResendVerification { 1748 + .map_err(map_sqlx_error)? 1749 + .map(|row| { 1750 + Ok(UserResendVerification { 1703 1751 id: row.id, 1704 - handle: Handle::from(row.handle), 1752 + handle: column(row.handle, col::USERS_HANDLE)?, 1705 1753 email: row.email, 1706 1754 channel: row.channel, 1707 1755 discord_username: row.discord_username, ··· 1715 1763 ), 1716 1764 }) 1717 1765 }) 1766 + .transpose() 1718 1767 } 1719 1768 1720 1769 async fn set_channel_verified(&self, did: &Did, channel: CommsChannel) -> Result<(), DbError> { ··· 1760 1809 } 1761 1810 1762 1811 async fn get_handles_by_email(&self, email: &str) -> Result<Vec<Handle>, DbError> { 1763 - sqlx::query_scalar!( 1812 + let handles = sqlx::query_scalar!( 1764 1813 "SELECT handle FROM users WHERE LOWER(email) = LOWER($1) AND deactivated_at IS NULL ORDER BY created_at DESC", 1765 1814 email 1766 1815 ) 1767 1816 .fetch_all(&self.pool) 1768 1817 .await 1769 - .map(|handles| handles.into_iter().map(Handle::from).collect()) 1770 - .map_err(map_sqlx_error) 1818 + .map_err(map_sqlx_error)?; 1819 + 1820 + Ok(handles 1821 + .into_iter() 1822 + .filter_map(|h| legacy_column(h, col::USERS_HANDLE)) 1823 + .collect()) 1771 1824 } 1772 1825 1773 1826 async fn set_password_reset_code( ··· 1798 1851 ) 1799 1852 .fetch_optional(&self.pool) 1800 1853 .await 1801 - .map_err(map_sqlx_error) 1802 - .map(|opt| { 1803 - opt.map(|row| UserResetCodeInfo { 1854 + .map_err(map_sqlx_error)? 1855 + .map(|row| { 1856 + Ok(UserResetCodeInfo { 1804 1857 id: row.id, 1805 - did: Did::from(row.did), 1858 + did: column(row.did, col::USERS_DID)?, 1806 1859 preferred_comms_channel: row.preferred_comms_channel, 1807 1860 expires_at: row.password_reset_code_expires_at, 1808 1861 }) 1809 1862 }) 1863 + .transpose() 1810 1864 } 1811 1865 1812 1866 async fn clear_password_reset_code(&self, user_id: Uuid) -> Result<(), DbError> { ··· 1894 1948 .await 1895 1949 .map_err(map_sqlx_error)?; 1896 1950 1951 + let did = column(user_did, col::USERS_DID)?; 1952 + 1897 1953 tx.commit().await.map_err(map_sqlx_error)?; 1898 1954 1899 - Ok(PasswordResetResult { 1900 - did: Did::from(user_did), 1901 - session_jtis, 1902 - }) 1955 + Ok(PasswordResetResult { did, session_jtis }) 1903 1956 } 1904 1957 1905 1958 async fn activate_account(&self, did: &Did) -> Result<bool, DbError> { ··· 2004 2057 ) 2005 2058 .fetch_optional(&self.pool) 2006 2059 .await 2007 - .map_err(map_sqlx_error) 2008 - .map(|opt| { 2009 - opt.map(|row| UserForDeletion { 2060 + .map_err(map_sqlx_error)? 2061 + .map(|row| { 2062 + Ok(UserForDeletion { 2010 2063 id: row.id, 2011 2064 password_hash: row.password_hash.map(PasswordHash::new), 2012 - handle: Handle::from(row.handle), 2065 + handle: column(row.handle, col::USERS_HANDLE)?, 2013 2066 }) 2014 2067 }) 2068 + .transpose() 2015 2069 } 2016 2070 2017 2071 async fn get_user_key_by_did(&self, did: &Did) -> Result<Option<UserKeyInfo>, DbError> { ··· 2154 2208 .await 2155 2209 .map_err(map_sqlx_error)?; 2156 2210 2157 - Ok(row.map(|r| UserForDidDoc { 2158 - id: r.id, 2159 - handle: Handle::from(r.handle), 2160 - deactivated_at: r.deactivated_at, 2161 - })) 2211 + row.map(|r| { 2212 + Ok(UserForDidDoc { 2213 + id: r.id, 2214 + handle: column(r.handle, col::USERS_HANDLE)?, 2215 + deactivated_at: r.deactivated_at, 2216 + }) 2217 + }) 2218 + .transpose() 2162 2219 } 2163 2220 2164 2221 async fn get_user_for_did_doc_build( ··· 2173 2230 .await 2174 2231 .map_err(map_sqlx_error)?; 2175 2232 2176 - Ok(row.map(|r| UserForDidDocBuild { 2177 - id: r.id, 2178 - handle: Handle::from(r.handle), 2179 - migrated_to_pds: r.migrated_to_pds, 2180 - })) 2233 + row.map(|r| { 2234 + Ok(UserForDidDocBuild { 2235 + id: r.id, 2236 + handle: column(r.handle, col::USERS_HANDLE)?, 2237 + migrated_to_pds: r.migrated_to_pds, 2238 + }) 2239 + }) 2240 + .transpose() 2181 2241 } 2182 2242 2183 2243 async fn upsert_did_web_overrides( ··· 2234 2294 .await 2235 2295 .map_err(map_sqlx_error)?; 2236 2296 2237 - Ok(row.map(|r| UserForPasskeySetup { 2238 - id: r.id, 2239 - handle: Handle::from(r.handle), 2240 - recovery_token: r.recovery_token, 2241 - recovery_token_expires_at: r.recovery_token_expires_at, 2242 - password_required: r.password_required, 2243 - })) 2297 + row.map(|r| { 2298 + Ok(UserForPasskeySetup { 2299 + id: r.id, 2300 + handle: column(r.handle, col::USERS_HANDLE)?, 2301 + recovery_token: r.recovery_token, 2302 + recovery_token_expires_at: r.recovery_token_expires_at, 2303 + password_required: r.password_required, 2304 + }) 2305 + }) 2306 + .transpose() 2244 2307 } 2245 2308 2246 2309 async fn get_user_for_passkey_recovery( ··· 2257 2320 .await 2258 2321 .map_err(map_sqlx_error)?; 2259 2322 2260 - Ok(row.map(|r| UserForPasskeyRecovery { 2261 - id: r.id, 2262 - did: Did::from(r.did), 2263 - handle: Handle::from(r.handle), 2264 - password_required: r.password_required, 2265 - })) 2323 + row.map(|r| { 2324 + Ok(UserForPasskeyRecovery { 2325 + id: r.id, 2326 + did: column(r.did, col::USERS_DID)?, 2327 + handle: column(r.handle, col::USERS_HANDLE)?, 2328 + password_required: r.password_required, 2329 + }) 2330 + }) 2331 + .transpose() 2266 2332 } 2267 2333 2268 2334 async fn set_recovery_token( ··· 2292 2358 .await 2293 2359 .map_err(map_sqlx_error)?; 2294 2360 2295 - Ok(row.map(|r| UserForRecovery { 2296 - id: r.id, 2297 - did: Did::from(r.did), 2298 - preferred_comms_channel: r.preferred_comms_channel, 2299 - recovery_token: r.recovery_token, 2300 - recovery_token_expires_at: r.recovery_token_expires_at, 2301 - })) 2361 + row.map(|r| { 2362 + Ok(UserForRecovery { 2363 + id: r.id, 2364 + did: column(r.did, col::USERS_DID)?, 2365 + preferred_comms_channel: r.preferred_comms_channel, 2366 + recovery_token: r.recovery_token, 2367 + recovery_token_expires_at: r.recovery_token_expires_at, 2368 + }) 2369 + }) 2370 + .transpose() 2302 2371 } 2303 2372 2304 2373 async fn get_accounts_scheduled_for_deletion( ··· 2322 2391 2323 2392 Ok(rows 2324 2393 .into_iter() 2325 - .map(|r| tranquil_db_traits::ScheduledDeletionAccount { 2326 - id: r.id, 2327 - did: Did::from(r.did), 2328 - handle: Handle::from(r.handle), 2394 + .filter_map(|r| { 2395 + Some(tranquil_db_traits::ScheduledDeletionAccount { 2396 + id: r.id, 2397 + did: legacy_column(r.did, col::USERS_DID)?, 2398 + handle: legacy_column(r.handle, col::USERS_HANDLE)?, 2399 + }) 2329 2400 }) 2330 2401 .collect()) 2331 2402 } ··· 3029 3100 )); 3030 3101 } 3031 3102 3103 + let old_handle = legacy_column(old_handle, col::USERS_HANDLE); 3104 + 3032 3105 tx.commit() 3033 3106 .await 3034 3107 .map_err(|e| tranquil_db_traits::MigrationReactivationError::Database(e.to_string()))?; 3035 3108 3036 3109 Ok(tranquil_db_traits::ReactivatedAccountInfo { 3037 3110 user_id: account_id, 3038 - old_handle: Handle::from(old_handle), 3111 + old_handle, 3039 3112 }) 3040 3113 } 3041 3114
+1 -3
crates/tranquil-store/src/metastore/delegation_ops.rs
··· 306 306 if let Some(val) = grant_val.filter(|v| v.revoked_at_ms.is_none()) { 307 307 let delegated_did = Did::new(val.delegated_did.clone()) 308 308 .map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?; 309 - let handle = self 310 - .resolve_handle_for_did(&val.delegated_did) 311 - .unwrap_or_else(|| Handle::new("unknown.invalid").unwrap()); 309 + let handle = self.resolve_handle_for_did(&val.delegated_did); 312 310 acc.push(DelegatedAccountInfo { 313 311 did: delegated_did, 314 312 handle,
+7 -3
crates/tranquil-store/src/metastore/user_ops.rs
··· 460 460 .map_err(|_| MetastoreError::CorruptData("invalid user did"))?, 461 461 email: v.email.clone(), 462 462 email_verified: v.email_verified, 463 - handle: Handle::new(v.handle.clone()) 464 - .map_err(|_| MetastoreError::CorruptData("invalid user handle"))?, 465 463 }) 466 464 }) 467 465 .transpose() ··· 3000 2998 } 3001 2999 3002 3000 let old_handle = Handle::new(user.handle.clone()) 3003 - .map_err(|_| MigrationReactivationError::Database("invalid handle".to_owned()))?; 3001 + .inspect_err(|_| { 3002 + tracing::warn!( 3003 + handle = %user.handle, 3004 + "ignoring a stored handle that isn't valid" 3005 + ); 3006 + }) 3007 + .ok(); 3004 3008 let user_id = user.id; 3005 3009 3006 3010 let mut batch = self.db.batch();
+2 -2
frontend/src/components/dashboard/ControllersContent.svelte
··· 26 26 27 27 interface ControlledAccount { 28 28 did: Did 29 - handle: Handle 29 + handle?: Handle 30 30 grantedScopes: ScopeSet 31 31 grantedAt: string 32 32 } ··· 505 505 <div class="item-card"> 506 506 <div class="item-info"> 507 507 <div class="item-header"> 508 - <span class="item-handle">@{account.handle}</span> 508 + <span class="item-handle">{account.handle ? `@${account.handle}` : account.did}</span> 509 509 <span class="badge scope">{getScopeLabel(account.grantedScopes)}</span> 510 510 </div> 511 511 <div class="item-details">
+1 -1
frontend/src/lib/api.ts
··· 355 355 const a = raw as Record<string, unknown>; 356 356 return { 357 357 did: unsafeAsDid(a.did as string), 358 - handle: unsafeAsHandle(a.handle as string), 358 + handle: a.handle ? unsafeAsHandle(a.handle as string) : undefined, 359 359 grantedScopes: unsafeAsScopeSet( 360 360 (a.granted_scopes ?? a.grantedScopes) as string, 361 361 ),
+1 -1
frontend/src/lib/types/api.ts
··· 529 529 530 530 export interface DelegationControlledAccount { 531 531 did: Did; 532 - handle: Handle; 532 + handle?: Handle; 533 533 grantedScopes: ScopeSet; 534 534 grantedAt: ISODateString; 535 535 }
+1 -1
frontend/src/routes/ActAs.svelte
··· 58 58 state: state, 59 59 code_challenge: codeChallenge, 60 60 code_challenge_method: 'S256', 61 - login_hint: account.handle 61 + login_hint: account.handle ?? account.did 62 62 }) 63 63 }) 64 64