From dcdef508dea6aca01f9faf22a779cc0745ceaaee Mon Sep 17 00:00:00 2001 From: lewis Date: Tue, 3 Mar 2026 16:07:57 +0200 Subject: [PATCH] fix: first user invite code --- .../tranquil-pds/src/api/identity/account.rs | 58 ++++++++++++------- crates/tranquil-pds/src/api/server/invite.rs | 4 +- .../src/api/server/passkey_account.rs | 16 ++++- crates/tranquil-pds/src/state.rs | 23 +++++++- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/crates/tranquil-pds/src/api/identity/account.rs b/crates/tranquil-pds/src/api/identity/account.rs index ceb9edc..826637e 100644 --- a/crates/tranquil-pds/src/api/identity/account.rs +++ b/crates/tranquil-pds/src/api/identity/account.rs @@ -548,28 +548,38 @@ pub async fn create_account( return ApiError::HandleTaken.into_response(); } - let invite_code_required = tranquil_config::get().server.invite_code_required; - if invite_code_required - && input - .invite_code - .as_ref() - .map(|c| c.trim().is_empty()) - .unwrap_or(true) - { - return ApiError::InviteCodeRequired.into_response(); - } - if let Some(code) = &input.invite_code - && !code.trim().is_empty() - { - let valid = match state.user_repo.check_and_consume_invite_code(code).await { - Ok(v) => v, - Err(e) => { - error!("Error checking invite code: {:?}", e); - return ApiError::InternalError(None).into_response(); + let is_bootstrap = state.bootstrap_invite_code.is_some() + && state.user_repo.count_users().await.unwrap_or(1) == 0; + + if is_bootstrap { + match input.invite_code.as_deref() { + Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => {} + _ => return ApiError::InvalidInviteCode.into_response(), + } + } else { + let invite_code_required = tranquil_config::get().server.invite_code_required; + if invite_code_required + && input + .invite_code + .as_ref() + .map(|c| c.trim().is_empty()) + .unwrap_or(true) + { + return ApiError::InviteCodeRequired.into_response(); + } + if let Some(code) = &input.invite_code + && !code.trim().is_empty() + { + let valid = match state.user_repo.check_and_consume_invite_code(code).await { + Ok(v) => v, + Err(e) => { + error!("Error checking invite code: {:?}", e); + return ApiError::InternalError(None).into_response(); + } + }; + if !valid { + return ApiError::InvalidInviteCode.into_response(); } - }; - if !valid { - return ApiError::InvalidInviteCode.into_response(); } } @@ -678,7 +688,11 @@ pub async fn create_account( commit_cid: commit_cid_str.clone(), repo_rev: rev_str.clone(), genesis_block_cids, - invite_code: input.invite_code.clone(), + invite_code: if is_bootstrap { + None + } else { + input.invite_code.clone() + }, birthdate_pref, }; diff --git a/crates/tranquil-pds/src/api/server/invite.rs b/crates/tranquil-pds/src/api/server/invite.rs index 35ee9a1..b904dac 100644 --- a/crates/tranquil-pds/src/api/server/invite.rs +++ b/crates/tranquil-pds/src/api/server/invite.rs @@ -14,7 +14,7 @@ use tracing::error; const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567"; -fn gen_random_token() -> String { +pub(crate) fn gen_random_token() -> String { let mut rng = rand::thread_rng(); let gen_segment = |rng: &mut rand::rngs::ThreadRng, len: usize| -> String { (0..len) @@ -24,7 +24,7 @@ fn gen_random_token() -> String { format!("{}-{}", gen_segment(&mut rng, 5), gen_segment(&mut rng, 5)) } -fn gen_invite_code() -> String { +pub fn gen_invite_code() -> String { let hostname = &tranquil_config::get().server.hostname; let hostname_prefix = hostname.replace('.', "-"); format!("{}-{}", hostname_prefix, gen_random_token()) diff --git a/crates/tranquil-pds/src/api/server/passkey_account.rs b/crates/tranquil-pds/src/api/server/passkey_account.rs index b06f4be..73627fa 100644 --- a/crates/tranquil-pds/src/api/server/passkey_account.rs +++ b/crates/tranquil-pds/src/api/server/passkey_account.rs @@ -146,7 +146,15 @@ pub async fn create_passkey_account( return ApiError::InvalidEmail.into_response(); } - let _validated_invite_code = if let Some(ref code) = input.invite_code { + let is_bootstrap = state.bootstrap_invite_code.is_some() + && state.user_repo.count_users().await.unwrap_or(1) == 0; + + let _validated_invite_code = if is_bootstrap { + match input.invite_code.as_deref() { + Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => None, + _ => return ApiError::InvalidInviteCode.into_response(), + } + } else if let Some(ref code) = input.invite_code { match state.infra_repo.validate_invite_code(code).await { Ok(validated) => Some(validated), Err(_) => return ApiError::InvalidInviteCode.into_response(), @@ -447,7 +455,11 @@ pub async fn create_passkey_account( commit_cid: commit_cid.to_string(), repo_rev: rev.as_ref().to_string(), genesis_block_cids, - invite_code: input.invite_code.clone(), + invite_code: if is_bootstrap { + None + } else { + input.invite_code.clone() + }, birthdate_pref, }; diff --git a/crates/tranquil-pds/src/state.rs b/crates/tranquil-pds/src/state.rs index bf5b33a..a2a474d 100644 --- a/crates/tranquil-pds/src/state.rs +++ b/crates/tranquil-pds/src/state.rs @@ -58,6 +58,7 @@ pub struct AppState { pub sso_manager: SsoManager, pub webauthn_config: Arc, pub shutdown: CancellationToken, + pub bootstrap_invite_code: Option, } #[derive(Debug, Clone, Copy)] @@ -232,7 +233,26 @@ impl AppState { .await .map_err(|e| format!("Failed to run migrations: {}", e))?; - Ok(Self::from_db(db, shutdown).await) + let bootstrap_invite_code = match ( + cfg.server.invite_code_required, + sqlx::query_scalar!("SELECT COUNT(*) FROM users") + .fetch_one(&db) + .await, + ) { + (true, Ok(Some(0))) => { + let code = crate::api::server::invite::gen_invite_code(); + tracing::info!( + "No users exist and invite codes are required. Bootstrap invite code: {}", + code + ); + Some(code) + } + _ => None, + }; + + let mut state = Self::from_db(db, shutdown).await; + state.bootstrap_invite_code = bootstrap_invite_code; + Ok(state) } pub async fn from_db(db: PgPool, shutdown: CancellationToken) -> Self { @@ -285,6 +305,7 @@ impl AppState { sso_manager, webauthn_config, shutdown, + bootstrap_invite_code: None, } }