From fbfa15b0b4289c2f239c88a65447c1389a7f6c7c Mon Sep 17 00:00:00 2001 From: Lewis Date: Sun, 12 Jul 2026 08:03:14 +0200 Subject: [PATCH] invite: newtype InviteCode for invite endpoints + store Lewis: May this revision serve well! --- crates/tranquil-api/src/admin/account/info.rs | 16 ++-- crates/tranquil-api/src/admin/invite.rs | 20 ++--- crates/tranquil-api/src/common.rs | 2 +- crates/tranquil-api/src/server/invite.rs | 3 +- crates/tranquil-db-traits/src/infra.rs | 35 +++++--- crates/tranquil-db-traits/src/invite_code.rs | 14 ++- crates/tranquil-db-traits/src/user.rs | 6 +- crates/tranquil-db/src/postgres/infra.rs | 87 +++++++++++++------ .../src/endpoints/authorize/login.rs | 3 +- .../src/endpoints/authorize/passkey.rs | 3 +- .../src/endpoints/authorize/two_factor.rs | 6 +- crates/tranquil-pds/src/api/invite.rs | 15 ++-- crates/tranquil-pds/src/state.rs | 2 +- crates/tranquil-pds/src/util.rs | 4 +- .../tranquil-pds/tests/invite_registration.rs | 9 +- crates/tranquil-pds/tests/store_parity.rs | 2 +- crates/tranquil-store/src/metastore/client.rs | 39 ++++++--- .../tranquil-store/src/metastore/handler.rs | 60 ++++++------- .../tranquil-store/src/metastore/infra_ops.rs | 18 ++-- 19 files changed, 201 insertions(+), 143 deletions(-) diff --git a/crates/tranquil-api/src/admin/account/info.rs b/crates/tranquil-api/src/admin/account/info.rs index f340c14..268aec2 100644 --- a/crates/tranquil-api/src/admin/account/info.rs +++ b/crates/tranquil-api/src/admin/account/info.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use tranquil_pds::api::error::{ApiError, DbResultExt}; use tranquil_pds::auth::{Admin, Auth}; use tranquil_pds::state::AppState; -use tranquil_pds::types::{Did, Handle}; +use tranquil_pds::types::{Did, Handle, InviteCode}; #[derive(Deserialize)] pub struct GetAccountInfoParams { @@ -39,7 +39,7 @@ pub struct AccountInfo { #[derive(Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct InviteCodeInfo { - pub code: String, + pub code: InviteCode, pub available: i32, pub disabled: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -123,12 +123,12 @@ async fn get_invites_for_user( return None; } - let code_strings: Vec = invite_codes.iter().map(|ic| ic.code.clone()).collect(); + let codes: Vec = invite_codes.iter().map(|ic| ic.code.clone()).collect(); let uses = state .repos .infra - .get_invite_code_uses_batch(&code_strings) + .get_invite_code_uses_batch(&codes) .await .ok()?; @@ -157,7 +157,7 @@ async fn get_invites_for_user( } } -async fn get_invite_code_info(state: &AppState, code: &str) -> Option { +async fn get_invite_code_info(state: &AppState, code: &InviteCode) -> Option { let info = state.repos.infra.get_invite_code_info(code).await.ok()??; let uses = state @@ -217,7 +217,7 @@ pub async fn get_account_infos( .await .unwrap_or_default(); - let all_codes: Vec = all_invite_codes + let all_codes: Vec = all_invite_codes .iter() .map(|(_, c)| c.code.clone()) .collect(); @@ -233,7 +233,7 @@ pub async fn get_account_infos( Vec::new() }; - let invited_by_map: HashMap = state + let invited_by_map: HashMap = state .repos .infra .get_invite_code_uses_by_users(&user_ids) @@ -249,7 +249,7 @@ pub async fn get_account_infos( let (codes_by_user, code_info_map): ( HashMap>, - HashMap, + HashMap, ) = all_invite_codes.into_iter().fold( (HashMap::new(), HashMap::new()), |(mut by_user, mut by_code), (user_id, ic)| { diff --git a/crates/tranquil-api/src/admin/invite.rs b/crates/tranquil-api/src/admin/invite.rs index a5415f4..1de24d7 100644 --- a/crates/tranquil-api/src/admin/invite.rs +++ b/crates/tranquil-api/src/admin/invite.rs @@ -10,11 +10,12 @@ use tranquil_pds::api::EmptyResponse; use tranquil_pds::api::error::{ApiError, DbResultExt}; use tranquil_pds::auth::{Admin, Auth}; use tranquil_pds::state::AppState; +use tranquil_types::{Did, InviteCode}; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct DisableInviteCodesInput { - pub codes: Option>, + pub codes: Option>, pub accounts: Option>, } @@ -34,11 +35,10 @@ pub async fn disable_invite_codes( if let Err(e) = state .repos .infra - .disable_invite_codes_by_account(&accounts_typed) + .disable_invite_codes_by_account(accounts) .await - { - error!("DB error disabling invite codes by account: {:?}", e); - } + { + error!("DB error disabling invite codes by account: {:?}", e); } Ok(Json(EmptyResponse {})) } @@ -53,7 +53,7 @@ pub struct GetInviteCodesParams { #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct InviteCodeInfo { - pub code: String, + pub code: InviteCode, pub available: i32, pub disabled: bool, pub for_account: String, @@ -72,7 +72,7 @@ pub struct InviteCodeUseInfo { #[derive(Serialize)] pub struct GetInviteCodesOutput { #[serde(skip_serializing_if = "Option::is_none")] - pub cursor: Option, + pub cursor: Option, pub codes: Vec, } @@ -95,7 +95,7 @@ pub async fn get_invite_codes( .log_db_err("fetching invite codes")?; let user_ids: Vec = codes_rows.iter().map(|r| r.created_by_user).collect(); - let code_strings: Vec = codes_rows.iter().map(|r| r.code.clone()).collect(); + let code_values: Vec = codes_rows.iter().map(|r| r.code.clone()).collect(); let creator_dids: std::collections::HashMap = state .repos @@ -106,14 +106,14 @@ pub async fn get_invite_codes( .into_iter() .collect(); - let uses_by_code = if code_strings.is_empty() { + let uses_by_code = if code_values.is_empty() { std::collections::HashMap::new() } else { common::group_invite_uses_by_code( state .repos .infra - .get_invite_code_uses_batch(&code_strings) + .get_invite_code_uses_batch(&code_values) .await .unwrap_or_default(), |u| InviteCodeUseInfo { diff --git a/crates/tranquil-api/src/common.rs b/crates/tranquil-api/src/common.rs index c7f4c00..45f446c 100644 --- a/crates/tranquil-api/src/common.rs +++ b/crates/tranquil-api/src/common.rs @@ -76,7 +76,7 @@ pub async fn resolve_repo_user_id( pub fn group_invite_uses_by_code( uses: Vec, map_use: F, -) -> HashMap> +) -> HashMap> where F: Fn(tranquil_db_traits::InviteCodeUse) -> U, { diff --git a/crates/tranquil-api/src/server/invite.rs b/crates/tranquil-api/src/server/invite.rs index c93625c..b435b1d 100644 --- a/crates/tranquil-api/src/server/invite.rs +++ b/crates/tranquil-api/src/server/invite.rs @@ -7,6 +7,7 @@ use tranquil_pds::auth::{Admin, Auth, NotTakendown}; use tranquil_pds::state::AppState; use tranquil_pds::types::Did; use tranquil_pds::util::gen_invite_code; +use tranquil_types::InviteCode as InviteCodeValue; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -112,7 +113,7 @@ pub async fn create_invite_codes( let infra_repo = state.repos.infra.clone(); let use_count = input.use_count; async move { - let codes: Vec = (0..code_count).map(|_| gen_invite_code()).collect(); + 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)) .await diff --git a/crates/tranquil-db-traits/src/infra.rs b/crates/tranquil-db-traits/src/infra.rs index 70e1bfa..d01c184 100644 --- a/crates/tranquil-db-traits/src/infra.rs +++ b/crates/tranquil-db-traits/src/infra.rs @@ -148,7 +148,7 @@ pub struct QueuedComms { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InviteCodeInfo { - pub code: String, + pub code: InviteCode, pub available_uses: i32, pub state: InviteCodeState, pub for_account: Option, @@ -158,7 +158,7 @@ pub struct InviteCodeInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InviteCodeUse { - pub code: String, + pub code: InviteCode, pub used_by_did: Did, pub used_by_handle: Option, pub used_at: DateTime, @@ -166,7 +166,7 @@ pub struct InviteCodeUse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InviteCodeRow { - pub code: String, + pub code: InviteCode, pub available_uses: i32, pub disabled: Option, pub created_by_user: Uuid, @@ -248,24 +248,27 @@ pub trait InfraRepository: Send + Sync { async fn create_invite_code( &self, - code: &str, + code: &InviteCode, use_count: i32, for_account: Option<&Did>, ) -> Result; async fn create_invite_codes_batch( &self, - codes: &[String], + codes: &[InviteCode], use_count: i32, created_by_user: Uuid, for_account: Option<&Did>, ) -> Result<(), DbError>; - async fn get_invite_code_available_uses(&self, code: &str) -> Result, DbError>; + async fn get_invite_code_available_uses( + &self, + code: &InviteCode, + ) -> Result, DbError>; async fn validate_invite_code<'a>( &self, - code: &'a str, + code: &'a InviteCode, ) -> Result, InviteCodeError>; async fn get_invite_codes_for_account( @@ -273,9 +276,9 @@ pub trait InfraRepository: Send + Sync { for_account: &Did, ) -> Result, DbError>; - async fn get_invite_code_uses(&self, code: &str) -> Result, DbError>; + async fn get_invite_code_uses(&self, code: &InviteCode) -> Result, DbError>; - async fn disable_invite_codes_by_code(&self, codes: &[String]) -> Result<(), DbError>; + async fn disable_invite_codes_by_code(&self, codes: &[InviteCode]) -> Result<(), DbError>; async fn disable_invite_codes_by_account(&self, accounts: &[Did]) -> Result<(), DbError>; @@ -290,7 +293,7 @@ pub trait InfraRepository: Send + Sync { async fn get_invite_code_uses_batch( &self, - codes: &[String], + codes: &[InviteCode], ) -> Result, DbError>; async fn get_invites_created_by_user( @@ -298,14 +301,20 @@ pub trait InfraRepository: Send + Sync { user_id: Uuid, ) -> Result, DbError>; - async fn get_invite_code_info(&self, code: &str) -> Result, DbError>; + async fn get_invite_code_info( + &self, + code: &InviteCode, + ) -> Result, DbError>; async fn get_invite_codes_by_users( &self, user_ids: &[Uuid], ) -> Result, DbError>; - async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result, DbError>; + async fn get_invite_code_used_by_user( + &self, + user_id: Uuid, + ) -> Result, DbError>; async fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), DbError>; @@ -425,7 +434,7 @@ pub trait InfraRepository: Send + Sync { async fn get_invite_code_uses_by_users( &self, user_ids: &[Uuid], - ) -> Result, DbError>; + ) -> Result, DbError>; async fn get_deletion_request_by_did( &self, diff --git a/crates/tranquil-db-traits/src/invite_code.rs b/crates/tranquil-db-traits/src/invite_code.rs index 8232e93..159db24 100644 --- a/crates/tranquil-db-traits/src/invite_code.rs +++ b/crates/tranquil-db-traits/src/invite_code.rs @@ -1,22 +1,18 @@ -use std::marker::PhantomData; +use tranquil_types::InviteCode; use crate::DbError; #[derive(Debug)] pub struct ValidatedInviteCode<'a> { - code: &'a str, - _marker: PhantomData<&'a ()>, + code: &'a InviteCode, } impl<'a> ValidatedInviteCode<'a> { - pub fn new_validated(code: &'a str) -> Self { - Self { - code, - _marker: PhantomData, - } + pub fn new_validated(code: &'a InviteCode) -> Self { + Self { code } } - pub fn code(&self) -> &str { + pub fn code(&self) -> &'a InviteCode { self.code } } diff --git a/crates/tranquil-db-traits/src/user.rs b/crates/tranquil-db-traits/src/user.rs index b853a8a..a3598e6 100644 --- a/crates/tranquil-db-traits/src/user.rs +++ b/crates/tranquil-db-traits/src/user.rs @@ -1010,7 +1010,7 @@ pub struct CreatePasswordAccountInput { pub commit_cid: CidLink, pub repo_rev: Tid, pub genesis_block_cids: Vec>, - pub invite_code: Option, + pub invite_code: Option, pub birthdate_pref: Option, } @@ -1062,7 +1062,7 @@ pub struct CreatePasskeyAccountInput { pub commit_cid: CidLink, pub repo_rev: Tid, pub genesis_block_cids: Vec>, - pub invite_code: Option, + pub invite_code: Option, pub birthdate_pref: Option, } @@ -1080,7 +1080,7 @@ pub struct CreateSsoAccountInput { pub commit_cid: CidLink, pub repo_rev: Tid, pub genesis_block_cids: Vec>, - pub invite_code: Option, + pub invite_code: Option, pub birthdate_pref: Option, pub sso_provider: SsoProviderType, pub sso_provider_user_id: String, diff --git a/crates/tranquil-db/src/postgres/infra.rs b/crates/tranquil-db/src/postgres/infra.rs index e9bfeb9..9f6130a 100644 --- a/crates/tranquil-db/src/postgres/infra.rs +++ b/crates/tranquil-db/src/postgres/infra.rs @@ -151,7 +151,7 @@ impl InfraRepository for PostgresInfraRepository { async fn create_invite_code( &self, - code: &str, + code: &InviteCode, use_count: i32, for_account: Option<&Did>, ) -> Result { @@ -172,16 +172,17 @@ impl InfraRepository for PostgresInfraRepository { async fn create_invite_codes_batch( &self, - codes: &[String], + codes: &[InviteCode], use_count: i32, created_by_user: Uuid, for_account: Option<&Did>, ) -> Result<(), DbError> { let for_account_str = for_account.map(|d| d.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) SELECT code, $2, $3, $4 FROM UNNEST($1::text[]) AS t(code)"#, - codes, + &code_strs, use_count, created_by_user, for_account_str @@ -193,7 +194,10 @@ impl InfraRepository for PostgresInfraRepository { Ok(()) } - async fn get_invite_code_available_uses(&self, code: &str) -> Result, DbError> { + async fn get_invite_code_available_uses( + &self, + code: &InviteCode, + ) -> Result, DbError> { let result = sqlx::query_scalar!( "SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE", code @@ -207,7 +211,7 @@ impl InfraRepository for PostgresInfraRepository { async fn validate_invite_code<'a>( &self, - code: &'a str, + code: &'a InviteCode, ) -> Result, InviteCodeError> { let result = sqlx::query!( r#"SELECT available_uses, COALESCE(disabled, false) as "disabled!" FROM invite_codes WHERE code = $1"#, @@ -249,7 +253,7 @@ impl InfraRepository for PostgresInfraRepository { Ok(results .into_iter() .map(|r| InviteCodeInfo { - code: r.code, + 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)), @@ -259,7 +263,7 @@ impl InfraRepository for PostgresInfraRepository { .collect()) } - async fn get_invite_code_uses(&self, code: &str) -> Result, DbError> { + async fn get_invite_code_uses(&self, code: &InviteCode) -> Result, DbError> { let results = sqlx::query!( r#"SELECT u.did, u.handle, icu.used_at FROM invite_code_uses icu @@ -283,10 +287,11 @@ impl InfraRepository for PostgresInfraRepository { .collect()) } - async fn disable_invite_codes_by_code(&self, codes: &[String]) -> Result<(), DbError> { + async fn disable_invite_codes_by_code(&self, codes: &[InviteCode]) -> Result<(), DbError> { + let code_strs: Vec = codes.iter().map(|c| c.to_string()).collect(); sqlx::query!( "UPDATE invite_codes SET disabled = TRUE WHERE code = ANY($1)", - codes + &code_strs ) .execute(&self.pool) .await @@ -315,9 +320,24 @@ impl InfraRepository for PostgresInfraRepository { limit: i64, sort: InviteCodeSortOrder, ) -> Result, DbError> { + fn to_row( + code: String, + available_uses: i32, + disabled: Option, + created_by_user: Uuid, + created_at: DateTime, + ) -> InviteCodeRow { + InviteCodeRow { + code: InviteCode::from(code), + available_uses, + disabled, + created_by_user, + created_at, + } + } + let results = match (cursor, sort) { - (Some(cursor_code), InviteCodeSortOrder::Recent) => sqlx::query_as!( - InviteCodeRow, + (Some(cursor_code), InviteCodeSortOrder::Recent) => sqlx::query!( r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at FROM invite_codes ic WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) @@ -339,9 +359,19 @@ impl InfraRepository for PostgresInfraRepository { ) .fetch_all(&self.pool) .await - .map_err(map_sqlx_error)?, - (Some(cursor_code), InviteCodeSortOrder::Usage) => sqlx::query_as!( - InviteCodeRow, + .map_err(map_sqlx_error)? + .into_iter() + .map(|r| { + to_row( + r.code, + r.available_uses, + r.disabled, + r.created_by_user, + r.created_at, + ) + }) + .collect(), + (Some(cursor_code), InviteCodeSortOrder::Usage) => sqlx::query!( r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at FROM invite_codes ic WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) @@ -383,15 +413,16 @@ impl InfraRepository for PostgresInfraRepository { async fn get_invite_code_uses_batch( &self, - codes: &[String], + codes: &[InviteCode], ) -> Result, DbError> { + let code_strs: Vec = codes.iter().map(|c| c.to_string()).collect(); let results = sqlx::query!( r#"SELECT icu.code, u.did, icu.used_at FROM invite_code_uses icu JOIN users u ON icu.used_by_user = u.id WHERE icu.code = ANY($1) ORDER BY icu.used_at DESC"#, - codes + &code_strs ) .fetch_all(&self.pool) .await @@ -400,7 +431,7 @@ impl InfraRepository for PostgresInfraRepository { Ok(results .into_iter() .map(|r| InviteCodeUse { - code: r.code, + code: InviteCode::from(r.code), used_by_did: Did::from(r.did), used_by_handle: None, used_at: r.used_at, @@ -426,7 +457,7 @@ impl InfraRepository for PostgresInfraRepository { Ok(results .into_iter() .map(|r| InviteCodeInfo { - code: r.code, + 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)), @@ -436,7 +467,10 @@ impl InfraRepository for PostgresInfraRepository { .collect()) } - async fn get_invite_code_info(&self, code: &str) -> Result, DbError> { + async fn get_invite_code_info( + &self, + code: &InviteCode, + ) -> Result, DbError> { let result = sqlx::query!( r#"SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by FROM invite_codes ic @@ -449,7 +483,7 @@ impl InfraRepository for PostgresInfraRepository { .map_err(map_sqlx_error)?; Ok(result.map(|r| InviteCodeInfo { - code: r.code, + 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)), @@ -480,7 +514,7 @@ impl InfraRepository for PostgresInfraRepository { ( r.created_by_user, InviteCodeInfo { - code: r.code, + 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)), @@ -492,7 +526,10 @@ impl InfraRepository for PostgresInfraRepository { .collect()) } - async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result, DbError> { + async fn get_invite_code_used_by_user( + &self, + user_id: Uuid, + ) -> Result, DbError> { let result = sqlx::query_scalar!( "SELECT code FROM invite_code_uses WHERE used_by_user = $1", user_id @@ -501,7 +538,7 @@ impl InfraRepository for PostgresInfraRepository { .await .map_err(map_sqlx_error)?; - Ok(result) + Ok(result.map(InviteCode::from)) } async fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), DbError> { @@ -1006,7 +1043,7 @@ impl InfraRepository for PostgresInfraRepository { async fn get_invite_code_uses_by_users( &self, user_ids: &[Uuid], - ) -> Result, DbError> { + ) -> Result, DbError> { let results = sqlx::query!( r#" SELECT used_by_user, code @@ -1021,7 +1058,7 @@ impl InfraRepository for PostgresInfraRepository { Ok(results .into_iter() - .map(|r| (r.used_by_user, r.code)) + .map(|r| (r.used_by_user, InviteCode::from(r.code))) .collect()) } diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs index 02335e6..01afa28 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs @@ -691,7 +691,6 @@ pub async fn authorize_post( } let code = AuthorizationCode::generate(); let auth_post_device_id = device_id.clone(); - let auth_post_code = AuthorizationCode::from(code.0.clone()); if state .repos .oauth @@ -699,7 +698,7 @@ pub async fn authorize_post( &form_request_id, &user.did, auth_post_device_id.as_ref(), - &auth_post_code, + &code, ) .await .is_err() diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs index 8521a6a..6eebc22 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs @@ -646,7 +646,6 @@ pub async fn passkey_finish( let code = AuthorizationCode::generate(); let passkey_final_device_id = device_id.clone(); - let passkey_final_code = AuthorizationCode::from(code.0.clone()); if state .repos .oauth @@ -654,7 +653,7 @@ pub async fn passkey_finish( &passkey_finish_request_id, &did, passkey_final_device_id.as_ref(), - &passkey_final_code, + &code, ) .await .is_err() diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs index 0d2bb96..4af40ce 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs @@ -165,7 +165,6 @@ pub async fn authorize_2fa_post( let code = AuthorizationCode::generate(); let device_id = extract_device_cookie(&headers); let twofa_totp_device_id = device_id.clone(); - let twofa_totp_code = AuthorizationCode::from(code.0.clone()); if state .repos .oauth @@ -173,7 +172,7 @@ pub async fn authorize_2fa_post( &twofa_post_request_id, &challenge.did, twofa_totp_device_id.as_ref(), - &twofa_totp_code, + &code, ) .await .is_err() @@ -310,7 +309,6 @@ pub async fn authorize_2fa_post( } let code = AuthorizationCode::generate(); let twofa_final_device_id = device_id.clone(); - let twofa_final_code = AuthorizationCode::from(code.0.clone()); if state .repos .oauth @@ -318,7 +316,7 @@ pub async fn authorize_2fa_post( &twofa_post_request_id, &did, twofa_final_device_id.as_ref(), - &twofa_final_code, + &code, ) .await .is_err() diff --git a/crates/tranquil-pds/src/api/invite.rs b/crates/tranquil-pds/src/api/invite.rs index 9bac06e..f141ea8 100644 --- a/crates/tranquil-pds/src/api/invite.rs +++ b/crates/tranquil-pds/src/api/invite.rs @@ -2,15 +2,16 @@ use tranquil_db_traits::InviteCodeError; use crate::api::error::ApiError; use crate::state::AppState; +use crate::types::InviteCode; #[derive(Debug, Clone, PartialEq, Eq)] pub enum InviteRegistration { Bootstrap, - Standard(Option), + Standard(Option), } impl InviteRegistration { - pub fn into_invite_code(self) -> Option { + pub fn into_invite_code(self) -> Option { match self { InviteRegistration::Bootstrap => None, InviteRegistration::Standard(code) => code, @@ -43,9 +44,13 @@ pub async fn check_registration_invite( } } - match invite_code.map(str::trim).filter(|code| !code.is_empty()) { - Some(code) => match state.repos.infra.validate_invite_code(code).await { - Ok(_) => Ok(InviteRegistration::Standard(Some(code.to_owned()))), + match invite_code + .map(str::trim) + .filter(|code| !code.is_empty()) + .map(InviteCode::new) + { + Some(code) => match state.repos.infra.validate_invite_code(&code).await { + Ok(_) => Ok(InviteRegistration::Standard(Some(code))), Err(InviteCodeError::DatabaseError(e)) => { tracing::error!("failed to validate invite code: {e:?}"); Err(ApiError::InternalError(None)) diff --git a/crates/tranquil-pds/src/state.rs b/crates/tranquil-pds/src/state.rs index d00da7c..c2fbd36 100644 --- a/crates/tranquil-pds/src/state.rs +++ b/crates/tranquil-pds/src/state.rs @@ -46,7 +46,7 @@ pub struct AppState { pub webauthn_config: Arc, pub cross_pds_oauth: Arc, pub shutdown: CancellationToken, - pub bootstrap_invite_code: Option, + pub bootstrap_invite_code: Option, pub signal_sender: Option>, pub signal_store_provider: Option>, pub eventlog_segments_dir: Option, diff --git a/crates/tranquil-pds/src/util.rs b/crates/tranquil-pds/src/util.rs index 57e7b75..dfaf57e 100644 --- a/crates/tranquil-pds/src/util.rs +++ b/crates/tranquil-pds/src/util.rs @@ -305,10 +305,10 @@ pub(crate) fn gen_invite_random_token() -> String { format!("{}-{}", gen_segment(&mut rng, 5), gen_segment(&mut rng, 5)) } -pub fn gen_invite_code() -> String { +pub fn gen_invite_code() -> crate::types::InviteCode { let hostname = &tranquil_config::get().server.hostname; let hostname_prefix = hostname.replace('.', "-"); - format!("{}-{}", hostname_prefix, gen_invite_random_token()) + crate::types::InviteCode::from(format!("{}-{}", hostname_prefix, gen_invite_random_token())) } pub fn is_self_hosted_did_web_enabled() -> bool { diff --git a/crates/tranquil-pds/tests/invite_registration.rs b/crates/tranquil-pds/tests/invite_registration.rs index 452a4d4..14fe597 100644 --- a/crates/tranquil-pds/tests/invite_registration.rs +++ b/crates/tranquil-pds/tests/invite_registration.rs @@ -4,8 +4,9 @@ use reqwest::{Client, StatusCode}; use serde_json::{Value, json}; use tranquil_pds::api::error::ApiError; use tranquil_pds::api::invite::{InviteRegistration, check_registration_invite}; +use tranquil_types::InviteCode; -async fn create_invite_code(client: &Client, admin_jwt: &str, use_count: u32) -> String { +async fn create_invite_code(client: &Client, admin_jwt: &str, use_count: u32) -> InviteCode { let res = client .post(format!( "{}/xrpc/com.atproto.server.createInviteCode", @@ -18,7 +19,7 @@ async fn create_invite_code(client: &Client, admin_jwt: &str, use_count: u32) -> .expect("failed to create invite code"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("invite code response not json"); - body["code"].as_str().expect("missing code").to_string() + InviteCode::from(body["code"].as_str().expect("missing code").to_string()) } #[tokio::test] @@ -30,7 +31,9 @@ async fn check_registration_invite_validates_without_consuming() { let code = create_invite_code(&client, &admin_jwt, 1).await; assert_eq!( - check_registration_invite(state, Some(&code)).await.unwrap(), + check_registration_invite(state, Some(code.as_str())) + .await + .unwrap(), InviteRegistration::Standard(Some(code.clone())) ); assert_eq!( diff --git a/crates/tranquil-pds/tests/store_parity.rs b/crates/tranquil-pds/tests/store_parity.rs index 59739c9..3247c55 100644 --- a/crates/tranquil-pds/tests/store_parity.rs +++ b/crates/tranquil-pds/tests/store_parity.rs @@ -1107,7 +1107,7 @@ async fn parity_invite_codes() { let did = test_did("invite"); let handle = test_handle("invite"); let _ = seed_repos(&f, &did, &handle).await; - let code = format!("parity-invite-{}", Uuid::new_v4()); + let code = tranquil_types::InviteCode::from(format!("parity-invite-{}", Uuid::new_v4())); let pg_created = f.pg.infra diff --git a/crates/tranquil-store/src/metastore/client.rs b/crates/tranquil-store/src/metastore/client.rs index 221933b..f563250 100644 --- a/crates/tranquil-store/src/metastore/client.rs +++ b/crates/tranquil-store/src/metastore/client.rs @@ -1834,7 +1834,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl async fn create_invite_code( &self, - code: &str, + code: &InviteCode, use_count: i32, for_account: Option<&Did>, ) -> Result { @@ -1851,7 +1851,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl async fn create_invite_codes_batch( &self, - codes: &[String], + codes: &[InviteCode], use_count: i32, created_by_user: Uuid, for_account: Option<&Did>, @@ -1869,7 +1869,10 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl recv(rx).await } - async fn get_invite_code_available_uses(&self, code: &str) -> Result, DbError> { + async fn get_invite_code_available_uses( + &self, + code: &InviteCode, + ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool.send(MetastoreRequest::Infra( InfraRequest::GetInviteCodeAvailableUses { @@ -1882,7 +1885,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl async fn validate_invite_code<'a>( &self, - code: &'a str, + code: &'a InviteCode, ) -> Result, InviteCodeError> { let (tx, rx) = oneshot::channel(); self.pool @@ -1909,7 +1912,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl recv(rx).await } - async fn get_invite_code_uses(&self, code: &str) -> Result, DbError> { + async fn get_invite_code_uses(&self, code: &InviteCode) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool .send(MetastoreRequest::Infra(InfraRequest::GetInviteCodeUses { @@ -1919,7 +1922,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl recv(rx).await } - async fn disable_invite_codes_by_code(&self, codes: &[String]) -> Result<(), DbError> { + async fn disable_invite_codes_by_code(&self, codes: &[InviteCode]) -> Result<(), DbError> { let (tx, rx) = oneshot::channel(); self.pool.send(MetastoreRequest::Infra( InfraRequest::DisableInviteCodesByCode { @@ -1970,7 +1973,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl async fn get_invite_code_uses_batch( &self, - codes: &[String], + codes: &[InviteCode], ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool.send(MetastoreRequest::Infra( @@ -1993,7 +1996,10 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl recv(rx).await } - async fn get_invite_code_info(&self, code: &str) -> Result, DbError> { + async fn get_invite_code_info( + &self, + code: &InviteCode, + ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool .send(MetastoreRequest::Infra(InfraRequest::GetInviteCodeInfo { @@ -2017,12 +2023,17 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl recv(rx).await } - async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result, DbError> { + async fn get_invite_code_used_by_user( + &self, + user_id: Uuid, + ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool.send(MetastoreRequest::Infra( InfraRequest::GetInviteCodeUsedByUser { user_id, tx }, ))?; - recv(rx).await + recv(rx) + .await + .map(|code: Option| code.map(InviteCode::from)) } async fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), DbError> { @@ -2382,7 +2393,7 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl async fn get_invite_code_uses_by_users( &self, user_ids: &[Uuid], - ) -> Result, DbError> { + ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool.send(MetastoreRequest::Infra( InfraRequest::GetInviteCodeUsesByUsers { @@ -2390,7 +2401,11 @@ impl tranquil_db_traits::InfraRepository for MetastoreCl tx, }, ))?; - recv(rx).await + recv(rx).await.map(|uses: Vec<(Uuid, String)>| { + uses.into_iter() + .map(|(id, code)| (id, InviteCode::from(code))) + .collect() + }) } async fn get_deletion_request_by_did( diff --git a/crates/tranquil-store/src/metastore/handler.rs b/crates/tranquil-store/src/metastore/handler.rs index 7bb185b..baa68d7 100644 --- a/crates/tranquil-store/src/metastore/handler.rs +++ b/crates/tranquil-store/src/metastore/handler.rs @@ -50,7 +50,7 @@ use crate::metastore::Metastore; type Tx = oneshot::Sender>; -fn reserve_invite(infra: &InfraOps, code: Option<&str>) -> Result<(), CreateAccountError> { +fn reserve_invite(infra: &InfraOps, code: Option<&InviteCode>) -> Result<(), CreateAccountError> { match code { Some(code) => infra.reserve_invite_code(code).map_err(|e| match e { InviteCodeError::DatabaseError(e) => CreateAccountError::Database(e.to_string()), @@ -62,7 +62,7 @@ fn reserve_invite(infra: &InfraOps, code: Option<&str>) -> Result<(), CreateAcco fn record_invite_use( infra: &InfraOps, - code: Option<&str>, + code: Option<&InviteCode>, user_id: Uuid, ) -> Result<(), CreateAccountError> { match code { @@ -76,7 +76,7 @@ fn record_invite_use( } } -fn refund_invite(infra: &InfraOps, code: Option<&str>) { +fn refund_invite(infra: &InfraOps, code: Option<&InviteCode>) { if let Some(code) = code && let Err(e) = infra.refund_invite_code(code) { @@ -86,7 +86,7 @@ fn refund_invite(infra: &InfraOps, code: Option<&str>) { fn finalize_account( infra: &InfraOps, - code: Option<&str>, + code: Option<&InviteCode>, created: Result, after: impl FnOnce(&T) -> Result, ) -> Result { @@ -1828,24 +1828,24 @@ pub enum InfraRequest { tx: Tx<()>, }, CreateInviteCode { - code: String, + code: InviteCode, use_count: i32, for_account: Option, tx: Tx, }, CreateInviteCodesBatch { - codes: Vec, + codes: Vec, use_count: i32, created_by_user: Uuid, for_account: Option, tx: Tx<()>, }, GetInviteCodeAvailableUses { - code: String, + code: InviteCode, tx: Tx>, }, ValidateInviteCode { - code: String, + code: InviteCode, tx: oneshot::Sender>, }, GetInviteCodesForAccount { @@ -5777,7 +5777,7 @@ fn dispatch_user(state: &HandlerState, req: UserReque } UserRequest::CreatePasswordAccount { input, tx } => { let infra = state.metastore.infra_ops(); - let code = input.invite_code.as_deref(); + let code = input.invite_code.as_ref(); let result = reserve_invite(&infra, code).and_then(|()| { finalize_account( &infra, @@ -5823,7 +5823,7 @@ fn dispatch_user(state: &HandlerState, req: UserReque } UserRequest::CreatePasskeyAccount { input, tx } => { let infra = state.metastore.infra_ops(); - let code = input.invite_code.as_deref(); + let code = input.invite_code.as_ref(); let result = reserve_invite(&infra, code).and_then(|()| { finalize_account( &infra, @@ -5844,7 +5844,7 @@ fn dispatch_user(state: &HandlerState, req: UserReque UserRequest::CreateSsoAccount { input, tx } => { let sso_ops = state.metastore.sso_ops(); let infra = state.metastore.infra_ops(); - let code = input.invite_code.as_deref(); + let code = input.invite_code.as_ref(); let result = sso_ops .consume_pending_registration(&input.pending_registration_token) .map_err(|e| CreateAccountError::Database(e.to_string())) @@ -6182,31 +6182,29 @@ mod tests { ) .unwrap(); let infra = metastore.infra_ops(); + let squid = InviteCode::new("squid-invite"); + let whelk = InviteCode::new("whelk"); - assert!(infra.create_invite_code("squid-invite", 1, None).unwrap()); + assert!(infra.create_invite_code(&squid, 1, None).unwrap()); - infra.reserve_invite_code("squid-invite").unwrap(); + infra.reserve_invite_code(&squid).unwrap(); assert_eq!( - infra - .get_invite_code_available_uses("squid-invite") - .unwrap(), + infra.get_invite_code_available_uses(&squid).unwrap(), Some(0) ); assert!(matches!( - infra.reserve_invite_code("squid-invite"), + infra.reserve_invite_code(&squid), Err(InviteCodeError::ExhaustedUses) )); assert_eq!( - infra - .get_invite_code_available_uses("squid-invite") - .unwrap(), + infra.get_invite_code_available_uses(&squid).unwrap(), Some(0), "a rejected reservation must not decrement below zero" ); assert!(matches!( - infra.reserve_invite_code("whelk"), + infra.reserve_invite_code(&whelk), Err(InviteCodeError::NotFound) )); } @@ -6222,29 +6220,27 @@ mod tests { ) .unwrap(); let infra = metastore.infra_ops(); + let squid = InviteCode::new("squid-invite"); + let whelk = InviteCode::new("whelk"); - assert!(infra.create_invite_code("squid-invite", 1, None).unwrap()); + assert!(infra.create_invite_code(&squid, 1, None).unwrap()); - infra.reserve_invite_code("squid-invite").unwrap(); - infra.refund_invite_code("squid-invite").unwrap(); + infra.reserve_invite_code(&squid).unwrap(); + infra.refund_invite_code(&squid).unwrap(); assert_eq!( - infra - .get_invite_code_available_uses("squid-invite") - .unwrap(), + infra.get_invite_code_available_uses(&squid).unwrap(), Some(1), "refund must return the reserved use so a failed signup does not burn it" ); - infra.reserve_invite_code("squid-invite").unwrap(); + infra.reserve_invite_code(&squid).unwrap(); assert_eq!( - infra - .get_invite_code_available_uses("squid-invite") - .unwrap(), + infra.get_invite_code_available_uses(&squid).unwrap(), Some(0) ); assert!(matches!( - infra.refund_invite_code("whelk"), + infra.refund_invite_code(&whelk), Err(InviteCodeError::NotFound) )); } diff --git a/crates/tranquil-store/src/metastore/infra_ops.rs b/crates/tranquil-store/src/metastore/infra_ops.rs index decd2c8..3020112 100644 --- a/crates/tranquil-store/src/metastore/infra_ops.rs +++ b/crates/tranquil-store/src/metastore/infra_ops.rs @@ -118,7 +118,7 @@ impl InfraOps { fn value_to_invite_info(&self, v: &InviteCodeValue) -> Result { Ok(InviteCodeInfo { - code: v.code.clone(), + code: InviteCode::from(v.code.clone()), available_uses: v.available_uses, state: InviteCodeState::from_disabled_flag(v.disabled), for_account: v @@ -360,7 +360,7 @@ impl InfraOps { pub fn create_invite_code( &self, - code: &str, + code: &InviteCode, use_count: i32, for_account: Option<&Did>, ) -> Result { @@ -390,7 +390,7 @@ impl InfraOps { pub fn create_invite_codes_batch( &self, - codes: &[String], + codes: &[InviteCode], use_count: i32, created_by_user: Uuid, for_account: Option<&Did>, @@ -418,7 +418,7 @@ impl InfraOps { pub fn get_invite_code_available_uses( &self, - code: &str, + code: &InviteCode, ) -> Result, MetastoreError> { let key = invite_code_key(code); let val: Option = point_lookup( @@ -432,7 +432,7 @@ impl InfraOps { pub fn validate_invite_code<'a>( &self, - code: &'a str, + code: &'a InviteCode, ) -> Result, InviteCodeError> { let key = invite_code_key(code); let val: Option = point_lookup( @@ -453,7 +453,7 @@ impl InfraOps { } } - pub fn reserve_invite_code(&self, code: &str) -> Result<(), InviteCodeError> { + pub fn reserve_invite_code(&self, code: &InviteCode) -> Result<(), InviteCodeError> { let _guard = self.counter_lock.lock(); let validated = self.validate_invite_code(code)?; self.decrement_invite_code_uses(&validated).map_err(|e| { @@ -461,7 +461,7 @@ impl InfraOps { }) } - pub fn refund_invite_code(&self, code: &str) -> Result<(), InviteCodeError> { + pub fn refund_invite_code(&self, code: &InviteCode) -> Result<(), InviteCodeError> { let _guard = self.counter_lock.lock(); let key = invite_code_key(code); let mut val: InviteCodeValue = point_lookup( @@ -557,7 +557,7 @@ impl InfraOps { .unwrap_or_else(|| Did::new("did:plc:unknown".to_owned()).unwrap()); let used_by_handle = self.resolve_handle_for_uuid(val.used_by); acc.push(InviteCodeUse { - code: code.to_owned(), + code: InviteCode::new(code), used_by_did, used_by_handle, used_at: DateTime::from_timestamp_millis(val.used_at_ms).unwrap_or_default(), @@ -628,7 +628,7 @@ impl InfraOps { .ok_or(MetastoreError::CorruptData("corrupt invite code"))?; let created_by_user = val.created_by.unwrap_or(Uuid::nil()); acc.push(InviteCodeRow { - code: val.code, + code: InviteCode::from(val.code), available_uses: val.available_uses, disabled: Some(val.disabled), created_by_user,