From 0c7cccb14cbc46f6dc9f0bf1b88686eb7ec94ee5 Mon Sep 17 00:00:00 2001 From: Lewis Date: Sun, 19 Jul 2026 17:19:22 +0300 Subject: [PATCH] invite: require owning account for generated codes Lewis: May this revision serve well! --- crates/tranquil-api/src/server/invite.rs | 36 +-- crates/tranquil-db-traits/src/infra.rs | 4 +- crates/tranquil-db/src/postgres/infra.rs | 209 ++++++++++-------- crates/tranquil-pds/tests/store_parity.rs | 22 +- crates/tranquil-store/src/metastore/client.rs | 8 +- .../tranquil-store/src/metastore/handler.rs | 12 +- .../tranquil-store/src/metastore/infra_ops.rs | 29 ++- ...60721_backfill_invite_code_for_account.sql | 21 ++ 8 files changed, 199 insertions(+), 142 deletions(-) create mode 100644 migrations/20260721_backfill_invite_code_for_account.sql diff --git a/crates/tranquil-api/src/server/invite.rs b/crates/tranquil-api/src/server/invite.rs index 28cdb3b..0235eab 100644 --- a/crates/tranquil-api/src/server/invite.rs +++ b/crates/tranquil-api/src/server/invite.rs @@ -43,7 +43,7 @@ pub async fn create_invite_code( match state .repos .infra - .create_invite_code(&code, input.use_count, Some(&for_account)) + .create_invite_code(&code, input.use_count, &for_account) .await { Ok(true) => Ok(Json(CreateInviteCodeOutput { code })), @@ -115,7 +115,7 @@ pub async fn create_invite_codes( async move { let codes: Vec = (0..code_count).map(|_| gen_invite_code()).collect(); infra_repo - .create_invite_codes_batch(&codes, use_count, admin_user_id, Some(&account)) + .create_invite_codes_batch(&codes, use_count, admin_user_id, &account) .await .map(|_| AccountCodes { account, codes }) } @@ -188,27 +188,24 @@ pub async fn get_account_invite_codes( let codes = futures::future::join_all(filtered_codes.into_iter().map(|info| { let infra_repo = state.repos.infra.clone(); async move { - let uses = infra_repo + let uses: Vec = infra_repo .get_invite_code_uses(&info.code) .await - .map(|use_rows| { - use_rows - .into_iter() - .map(|u| InviteCodeUse { - used_by: u.used_by_did.to_string(), - used_by_handle: u.used_by_handle.map(|h| h.to_string()), - used_at: u.used_at.to_rfc3339(), - }) - .collect::>() + .log_db_err("fetching invite code uses")? + .into_iter() + .map(|u| InviteCodeUse { + used_by: u.used_by_did.to_string(), + used_by_handle: u.used_by_handle.map(|h| h.to_string()), + used_at: u.used_at.to_rfc3339(), }) - .unwrap_or_default(); + .collect(); let use_count = i32::try_from(uses.len()).unwrap_or(i32::MAX); if !include_used && use_count >= info.available_uses { - return None; + return Ok(None); } - Some(InviteCode { + Ok(Some(InviteCode { code: info.code, available: info.available_uses, disabled: false, @@ -219,11 +216,16 @@ pub async fn get_account_invite_codes( .unwrap_or_else(|| "admin".to_string()), created_at: info.created_at.to_rfc3339(), uses, - }) + })) } })) .await; - let codes: Vec = codes.into_iter().flatten().collect(); + let codes: Vec = codes + .into_iter() + .collect::>, ApiError>>()? + .into_iter() + .flatten() + .collect(); Ok(Json(GetAccountInviteCodesOutput { codes })) } diff --git a/crates/tranquil-db-traits/src/infra.rs b/crates/tranquil-db-traits/src/infra.rs index d01c184..f6c5b15 100644 --- a/crates/tranquil-db-traits/src/infra.rs +++ b/crates/tranquil-db-traits/src/infra.rs @@ -250,7 +250,7 @@ pub trait InfraRepository: Send + Sync { &self, code: &InviteCode, use_count: i32, - for_account: Option<&Did>, + for_account: &Did, ) -> Result; async fn create_invite_codes_batch( @@ -258,7 +258,7 @@ pub trait InfraRepository: Send + Sync { codes: &[InviteCode], use_count: i32, created_by_user: Uuid, - for_account: Option<&Did>, + for_account: &Did, ) -> Result<(), DbError>; async fn get_invite_code_available_uses( diff --git a/crates/tranquil-db/src/postgres/infra.rs b/crates/tranquil-db/src/postgres/infra.rs index fe8e3a3..2ee3ec1 100644 --- a/crates/tranquil-db/src/postgres/infra.rs +++ b/crates/tranquil-db/src/postgres/infra.rs @@ -7,10 +7,12 @@ use tranquil_db_traits::{ InviteCodeSortOrder, InviteCodeState, InviteCodeUse, NotificationHistoryRow, PlcTokenInfo, QueuedComms, ReservedSigningKey, ReservedSigningKeyFull, ValidatedInviteCode, }; -use tranquil_types::{CidLink, Did, Handle, InviteCode}; +use tranquil_types::{CidLink, Did, InviteCode}; use uuid::Uuid; +use super::col; use super::user::map_sqlx_error; +use super::{column, legacy_column, opt_column}; pub struct PostgresInfraRepository { pool: PgPool, @@ -153,9 +155,9 @@ impl InfraRepository for PostgresInfraRepository { &self, code: &InviteCode, use_count: i32, - for_account: Option<&Did>, + for_account: &Did, ) -> Result { - let for_account_str = for_account.map(|d| d.as_str()); + let for_account_str = for_account.as_str(); let result = sqlx::query!( r#"INSERT INTO invite_codes (code, available_uses, created_by_user, for_account) SELECT $1, $2, id, $3 FROM users WHERE is_admin = true LIMIT 1"#, @@ -175,9 +177,9 @@ impl InfraRepository for PostgresInfraRepository { codes: &[InviteCode], use_count: i32, created_by_user: Uuid, - for_account: Option<&Did>, + for_account: &Did, ) -> Result<(), DbError> { - let for_account_str = for_account.map(|d| d.as_str()); + let for_account_str = for_account.as_str(); let code_strs: Vec = codes.iter().map(|c| c.to_string()).collect(); sqlx::query!( r#"INSERT INTO invite_codes (code, available_uses, created_by_user, for_account) @@ -250,17 +252,19 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(results + results .into_iter() - .map(|r| InviteCodeInfo { - code: InviteCode::from(r.code), - available_uses: r.available_uses, - state: InviteCodeState::from_optional_disabled_flag(r.disabled), - for_account: Some(Did::from(r.for_account)), - created_at: r.created_at, - created_by: None, + .map(|r| { + Ok(InviteCodeInfo { + code: InviteCode::from(r.code), + available_uses: r.available_uses, + state: InviteCodeState::from_optional_disabled_flag(r.disabled), + for_account: legacy_column(r.for_account, col::INVITE_CODES_FOR_ACCOUNT), + created_at: r.created_at, + created_by: None, + }) }) - .collect()) + .collect() } async fn get_invite_code_uses(&self, code: &InviteCode) -> Result, DbError> { @@ -278,11 +282,13 @@ impl InfraRepository for PostgresInfraRepository { Ok(results .into_iter() - .map(|r| InviteCodeUse { - code: code.clone(), - used_by_did: Did::from(r.did), - used_by_handle: Some(Handle::from(r.handle)), - used_at: r.used_at, + .filter_map(|r| { + Some(InviteCodeUse { + code: code.clone(), + used_by_did: legacy_column(r.did, col::USERS_DID)?, + used_by_handle: legacy_column(r.handle, col::USERS_HANDLE), + used_at: r.used_at, + }) }) .collect()) } @@ -436,10 +442,10 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(results + results .into_iter() - .map(|r| (r.id, Did::from(r.did))) - .collect()) + .map(|r| Ok((r.id, column(r.did, col::USERS_DID)?))) + .collect() } async fn get_invite_code_uses_batch( @@ -459,15 +465,17 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(results + results .into_iter() - .map(|r| InviteCodeUse { - code: InviteCode::from(r.code), - used_by_did: Did::from(r.did), - used_by_handle: None, - used_at: r.used_at, + .map(|r| { + Ok(InviteCodeUse { + code: InviteCode::from(r.code), + used_by_did: column(r.did, col::USERS_DID)?, + used_by_handle: None, + used_at: r.used_at, + }) }) - .collect()) + .collect() } async fn get_invites_created_by_user( @@ -485,17 +493,19 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(results + results .into_iter() - .map(|r| InviteCodeInfo { - code: InviteCode::from(r.code), - available_uses: r.available_uses, - state: InviteCodeState::from_optional_disabled_flag(r.disabled), - for_account: Some(Did::from(r.for_account)), - created_at: r.created_at, - created_by: Some(Did::from(r.created_by)), + .map(|r| { + Ok(InviteCodeInfo { + code: InviteCode::from(r.code), + available_uses: r.available_uses, + state: InviteCodeState::from_optional_disabled_flag(r.disabled), + for_account: legacy_column(r.for_account, col::INVITE_CODES_FOR_ACCOUNT), + created_at: r.created_at, + created_by: Some(column(r.created_by, col::USERS_DID)?), + }) }) - .collect()) + .collect() } async fn get_invite_code_info( @@ -513,14 +523,18 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(result.map(|r| InviteCodeInfo { - code: InviteCode::from(r.code), - available_uses: r.available_uses, - state: InviteCodeState::from_optional_disabled_flag(r.disabled), - for_account: Some(Did::from(r.for_account)), - created_at: r.created_at, - created_by: Some(Did::from(r.created_by)), - })) + result + .map(|r| { + Ok(InviteCodeInfo { + code: InviteCode::from(r.code), + available_uses: r.available_uses, + state: InviteCodeState::from_optional_disabled_flag(r.disabled), + for_account: legacy_column(r.for_account, col::INVITE_CODES_FOR_ACCOUNT), + created_at: r.created_at, + created_by: Some(column(r.created_by, col::USERS_DID)?), + }) + }) + .transpose() } async fn get_invite_codes_by_users( @@ -539,22 +553,22 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(results + results .into_iter() .map(|r| { - ( + Ok(( r.created_by_user, InviteCodeInfo { code: InviteCode::from(r.code), available_uses: r.available_uses, state: InviteCodeState::from_optional_disabled_flag(r.disabled), - for_account: Some(Did::from(r.for_account)), + for_account: legacy_column(r.for_account, col::INVITE_CODES_FOR_ACCOUNT), created_at: r.created_at, - created_by: Some(Did::from(r.created_by)), + created_by: Some(column(r.created_by, col::USERS_DID)?), }, - ) + )) }) - .collect()) + .collect() } async fn get_invite_code_used_by_user( @@ -683,10 +697,14 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(result.map(|r| DeletionRequest { - did: Did::from(r.did), - expires_at: r.expires_at, - })) + result + .map(|r| { + Ok(DeletionRequest { + did: column(r.did, col::ACCOUNT_DELETION_REQUESTS_DID)?, + expires_at: r.expires_at, + }) + }) + .transpose() } async fn delete_deletion_request(&self, token: &str) -> Result<(), DbError> { @@ -1027,16 +1045,20 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(result.map(|r| AdminAccountInfo { - id: r.id, - did: Did::from(r.did), - handle: Handle::from(r.handle), - email: r.email, - created_at: r.created_at, - invites_disabled: r.invites_disabled.unwrap_or(false), - email_verified: r.email_verified, - deactivated_at: r.deactivated_at, - })) + result + .map(|r| { + Ok(AdminAccountInfo { + 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, + invites_disabled: r.invites_disabled.unwrap_or(false), + email_verified: r.email_verified, + deactivated_at: r.deactivated_at, + }) + }) + .transpose() } async fn get_admin_account_infos_by_dids( @@ -1058,15 +1080,17 @@ impl InfraRepository for PostgresInfraRepository { Ok(results .into_iter() - .map(|r| AdminAccountInfo { - id: r.id, - did: Did::from(r.did), - handle: Handle::from(r.handle), - email: r.email, - created_at: r.created_at, - invites_disabled: r.invites_disabled.unwrap_or(false), - email_verified: r.email_verified, - deactivated_at: r.deactivated_at, + .filter_map(|r| { + Some(AdminAccountInfo { + id: r.id, + did: legacy_column(r.did, col::USERS_DID)?, + handle: legacy_column(r.handle, col::USERS_HANDLE)?, + email: r.email, + created_at: r.created_at, + invites_disabled: r.invites_disabled.unwrap_or(false), + email_verified: r.email_verified, + deactivated_at: r.deactivated_at, + }) }) .collect()) } @@ -1105,11 +1129,14 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| DeletionRequestWithToken { - token: r.token, - did: Did::new(r.did).expect("valid DID in database"), - expires_at: r.expires_at, - })) + row.map(|r| { + Ok(DeletionRequestWithToken { + token: r.token, + did: column(r.did, col::ACCOUNT_DELETION_REQUESTS_DID)?, + expires_at: r.expires_at, + }) + }) + .transpose() } async fn get_latest_comms_for_user( @@ -1202,14 +1229,20 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(row.map(|r| ReservedSigningKeyFull { - id: r.id, - did: r.did.map(|d| Did::new(d).expect("valid DID in database")), - public_key_did_key: Did::from(r.public_key_did_key), - private_key_bytes: r.private_key_bytes, - expires_at: r.expires_at, - used_at: r.used_at, - })) + row.map(|r| { + Ok(ReservedSigningKeyFull { + id: r.id, + did: opt_column(r.did, col::RESERVED_SIGNING_KEYS_DID)?, + public_key_did_key: column( + r.public_key_did_key, + col::RESERVED_SIGNING_KEYS_PUBLIC_KEY_DID_KEY, + )?, + private_key_bytes: r.private_key_bytes, + expires_at: r.expires_at, + used_at: r.used_at, + }) + }) + .transpose() } async fn get_plc_tokens_by_did(&self, did: &Did) -> Result, DbError> { diff --git a/crates/tranquil-pds/tests/store_parity.rs b/crates/tranquil-pds/tests/store_parity.rs index 15a5773..42334fc 100644 --- a/crates/tranquil-pds/tests/store_parity.rs +++ b/crates/tranquil-pds/tests/store_parity.rs @@ -117,7 +117,7 @@ fn test_handle(suffix: &str) -> Handle { } fn test_cid(seed: u8) -> CidLink { - CidLink::from(helpers::make_cid(&[seed])) + CidLink::from_cid(&helpers::make_cid(&[seed])) } fn test_nsid(name: &str) -> Nsid { @@ -139,7 +139,7 @@ fn test_at_uri(did: &Did, collection: &Nsid, rkey: &Rkey) -> AtUri { } async fn seed_user(repos: &PostgresRepositories, did: &Did, handle: &Handle) -> Uuid { - let commit_cid = CidLink::from(helpers::make_cid(did.as_str().as_bytes()).to_string()); + let commit_cid = CidLink::from_cid(&helpers::make_cid(did.as_str().as_bytes())); let input = tranquil_db_traits::CreatePasswordAccountInput { handle: handle.clone(), email: None, @@ -155,7 +155,7 @@ async fn seed_user(repos: &PostgresRepositories, did: &Did, handle: &Handle) -> encryption_version: 0, reserved_key_id: None, commit_cid, - repo_rev: Tid::from("rev0".to_string()), + repo_rev: Tid::new("3k2aaaaaaaaaa").expect("valid TID"), genesis_block_cids: vec![], invite_code: None, birthdate_pref: None, @@ -190,7 +190,7 @@ async fn seed_records( &collections, &rkeys, &cids, - &Tid::from("rev1".to_string()), + &Tid::new("3k2aaaaaaaaab").expect("valid TID"), ) .await .unwrap(); @@ -1109,15 +1109,11 @@ async fn parity_invite_codes() { let _ = seed_repos(&f, &did, &handle).await; let code = tranquil_types::InviteCode::from(format!("parity-invite-{}", Uuid::new_v4())); - let pg_created = - f.pg.infra - .create_invite_code(&code, 5, Some(&did)) - .await - .unwrap(); + let pg_created = f.pg.infra.create_invite_code(&code, 5, &did).await.unwrap(); let store_created = f .store .infra - .create_invite_code(&code, 5, Some(&did)) + .create_invite_code(&code, 5, &did) .await .unwrap(); assert_eq!(pg_created, store_created); @@ -1359,7 +1355,7 @@ async fn parity_signing_key_reservation() { let f = ParityFixture::new().await; let did = test_did("sigkey"); let expires = chrono::Utc::now() + chrono::Duration::hours(1); - let pub_key = Did::from(format!("did:key:z6Mk{}", Uuid::new_v4().simple())); + let pub_key = Did::new(format!("did:key:z6Mk{}", Uuid::new_v4().simple())).expect("valid DID"); let priv_bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; f.pg.infra @@ -1418,7 +1414,7 @@ async fn parity_repo_root_operations() { assert_eq!(pg_root, store_root); let new_root = test_cid(99); - let rev1 = Tid::from("rev1".to_string()); + let rev1 = Tid::new("3k2aaaaaaaaab").expect("valid TID"); f.pg.repo .update_repo_root(pg_uid, &new_root, &rev1) .await @@ -1632,7 +1628,7 @@ async fn parity_prune_events_older_than() { blobs: None, blocks: None, prev_data_cid: None, - rev: Some(Tid::from("rev0".to_string())), + rev: Some(Tid::new("3k2aaaaaaaaaa").expect("valid TID")), }; let baseline = f.pg.repo.get_max_seq().await.unwrap(); diff --git a/crates/tranquil-store/src/metastore/client.rs b/crates/tranquil-store/src/metastore/client.rs index 009af8d..902da21 100644 --- a/crates/tranquil-store/src/metastore/client.rs +++ b/crates/tranquil-store/src/metastore/client.rs @@ -1836,14 +1836,14 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl &self, code: &InviteCode, use_count: i32, - for_account: Option<&Did>, + for_account: &Did, ) -> Result { let (tx, rx) = oneshot::channel(); self.pool .send(MetastoreRequest::Infra(InfraRequest::CreateInviteCode { code: code.clone(), use_count, - for_account: for_account.cloned(), + for_account: for_account.clone(), tx, }))?; recv(rx).await @@ -1854,7 +1854,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl codes: &[InviteCode], use_count: i32, created_by_user: Uuid, - for_account: Option<&Did>, + for_account: &Did, ) -> Result<(), DbError> { let (tx, rx) = oneshot::channel(); self.pool.send(MetastoreRequest::Infra( @@ -1862,7 +1862,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl codes: codes.to_vec(), use_count, created_by_user, - for_account: for_account.cloned(), + for_account: for_account.clone(), tx, }, ))?; diff --git a/crates/tranquil-store/src/metastore/handler.rs b/crates/tranquil-store/src/metastore/handler.rs index 75029fc..e8dadcb 100644 --- a/crates/tranquil-store/src/metastore/handler.rs +++ b/crates/tranquil-store/src/metastore/handler.rs @@ -1828,14 +1828,14 @@ pub enum InfraRequest { CreateInviteCode { code: InviteCode, use_count: i32, - for_account: Option, + for_account: Did, tx: Tx, }, CreateInviteCodesBatch { codes: Vec, use_count: i32, created_by_user: Uuid, - for_account: Option, + for_account: Did, tx: Tx<()>, }, GetInviteCodeAvailableUses { @@ -3911,7 +3911,7 @@ fn dispatch_infra(state: &HandlerState, req: InfraRequest) { let result = state .metastore .infra_ops() - .create_invite_code(&code, use_count, for_account.as_ref()) + .create_invite_code(&code, use_count, &for_account) .map_err(metastore_to_db); let _ = tx.send(result); } @@ -3925,7 +3925,7 @@ fn dispatch_infra(state: &HandlerState, req: InfraRequest) { let result = state .metastore .infra_ops() - .create_invite_codes_batch(&codes, use_count, created_by_user, for_account.as_ref()) + .create_invite_codes_batch(&codes, use_count, created_by_user, &for_account) .map_err(metastore_to_db); let _ = tx.send(result); } @@ -6206,7 +6206,7 @@ mod tests { let owner = Did::new("did:plc:whelk").expect("valid DID"); let whelk = InviteCode::new("whelk"); - assert!(infra.create_invite_code(&squid, 1, Some(&owner)).unwrap()); + assert!(infra.create_invite_code(&squid, 1, &owner).unwrap()); infra.reserve_invite_code(&squid).unwrap(); assert_eq!( @@ -6245,7 +6245,7 @@ mod tests { let owner = Did::new("did:plc:whelk").expect("valid DID"); let whelk = InviteCode::new("whelk"); - assert!(infra.create_invite_code(&squid, 1, Some(&owner)).unwrap()); + assert!(infra.create_invite_code(&squid, 1, &owner).unwrap()); infra.reserve_invite_code(&squid).unwrap(); infra.refund_invite_code(&squid).unwrap(); diff --git a/crates/tranquil-store/src/metastore/infra_ops.rs b/crates/tranquil-store/src/metastore/infra_ops.rs index 6f1c056..d9f199c 100644 --- a/crates/tranquil-store/src/metastore/infra_ops.rs +++ b/crates/tranquil-store/src/metastore/infra_ops.rs @@ -362,7 +362,7 @@ impl InfraOps { &self, code: &InviteCode, use_count: i32, - for_account: Option<&Did>, + for_account: &Did, ) -> Result { let key = invite_code_key(code); let existing = self @@ -377,7 +377,7 @@ impl InfraOps { code: code.as_str().to_owned(), available_uses: use_count, disabled: false, - for_account: for_account.map(|d| d.to_string()), + for_account: Some(for_account.to_string()), created_by: None, created_at_ms: Utc::now().timestamp_millis(), }; @@ -393,7 +393,7 @@ impl InfraOps { codes: &[InviteCode], use_count: i32, created_by_user: Uuid, - for_account: Option<&Did>, + for_account: &Did, ) -> Result<(), MetastoreError> { let now_ms = Utc::now().timestamp_millis(); let mut batch = self.db.batch(); @@ -403,7 +403,7 @@ impl InfraOps { code: code.as_str().to_owned(), available_uses: use_count, disabled: false, - for_account: for_account.map(|d| d.to_string()), + for_account: Some(for_account.to_string()), created_by: Some(created_by_user), created_at_ms: now_ms, }; @@ -1458,14 +1458,19 @@ impl InfraOps { SigningKeyValue::deserialize, "corrupt signing key", )?; - Ok(val.map(|v| ReservedSigningKeyFull { - id: v.id, - did: v.did.and_then(|d| Did::new(d).ok()), - public_key_did_key: Did::from(v.public_key_did_key), - private_key_bytes: v.private_key_bytes, - expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), - used_at: v.used_at_ms.and_then(DateTime::from_timestamp_millis), - })) + val.map(|v| { + Ok(ReservedSigningKeyFull { + id: v.id, + did: v.did.and_then(|d| Did::new(d).ok()), + public_key_did_key: Did::new(v.public_key_did_key).map_err(|_| { + MetastoreError::CorruptData("corrupt reserved signing key public_key_did_key") + })?, + private_key_bytes: v.private_key_bytes, + expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), + used_at: v.used_at_ms.and_then(DateTime::from_timestamp_millis), + }) + }) + .transpose() } pub fn get_plc_tokens_for_user( diff --git a/migrations/20260721_backfill_invite_code_for_account.sql b/migrations/20260721_backfill_invite_code_for_account.sql new file mode 100644 index 0000000..f400627 --- /dev/null +++ b/migrations/20260721_backfill_invite_code_for_account.sql @@ -0,0 +1,21 @@ +-- The old insert path wrote for_account through without validating it +-- so an older row can contain any string at all +-- most often a handle or the literal 'admin' the column defaulted to +-- where the read path warns and drops whatever the Did newtype won't parse +-- so an unconverted row comes back with no for_account at all. +-- The first statement resolves the bare handles that still match a user +-- and the second covers every 'admin' row, since created_by_user is NOT NULL. +-- Nothing else converts +-- so a prefixed handle, a handle whose user has renamed since, +-- and anything that was never an identifier all stay as they are. +UPDATE invite_codes ic +SET for_account = u.did +FROM users u +WHERE LOWER(u.handle) = LOWER(ic.for_account) + AND ic.for_account NOT LIKE 'did:%'; + +UPDATE invite_codes ic +SET for_account = u.did +FROM users u +WHERE u.id = ic.created_by_user + AND ic.for_account = 'admin';