Log an invite code to the server, that the first user will use when they create their account.
+36
-22
crates/tranquil-pds/src/api/identity/account.rs
+36
-22
crates/tranquil-pds/src/api/identity/account.rs
···
548
548
return ApiError::HandleTaken.into_response();
549
549
}
550
550
551
-
let invite_code_required = tranquil_config::get().server.invite_code_required;
552
-
if invite_code_required
553
-
&& input
554
-
.invite_code
555
-
.as_ref()
556
-
.map(|c| c.trim().is_empty())
557
-
.unwrap_or(true)
558
-
{
559
-
return ApiError::InviteCodeRequired.into_response();
560
-
}
561
-
if let Some(code) = &input.invite_code
562
-
&& !code.trim().is_empty()
563
-
{
564
-
let valid = match state.user_repo.check_and_consume_invite_code(code).await {
565
-
Ok(v) => v,
566
-
Err(e) => {
567
-
error!("Error checking invite code: {:?}", e);
568
-
return ApiError::InternalError(None).into_response();
551
+
let is_bootstrap = state.bootstrap_invite_code.is_some()
552
+
&& state.user_repo.count_users().await.unwrap_or(1) == 0;
553
+
554
+
if is_bootstrap {
555
+
match input.invite_code.as_deref() {
556
+
Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => {}
557
+
_ => return ApiError::InvalidInviteCode.into_response(),
558
+
}
559
+
} else {
560
+
let invite_code_required = tranquil_config::get().server.invite_code_required;
561
+
if invite_code_required
562
+
&& input
563
+
.invite_code
564
+
.as_ref()
565
+
.map(|c| c.trim().is_empty())
566
+
.unwrap_or(true)
567
+
{
568
+
return ApiError::InviteCodeRequired.into_response();
569
+
}
570
+
if let Some(code) = &input.invite_code
571
+
&& !code.trim().is_empty()
572
+
{
573
+
let valid = match state.user_repo.check_and_consume_invite_code(code).await {
574
+
Ok(v) => v,
575
+
Err(e) => {
576
+
error!("Error checking invite code: {:?}", e);
577
+
return ApiError::InternalError(None).into_response();
578
+
}
579
+
};
580
+
if !valid {
581
+
return ApiError::InvalidInviteCode.into_response();
569
582
}
570
-
};
571
-
if !valid {
572
-
return ApiError::InvalidInviteCode.into_response();
573
583
}
574
584
}
575
585
···
678
688
commit_cid: commit_cid_str.clone(),
679
689
repo_rev: rev_str.clone(),
680
690
genesis_block_cids,
681
-
invite_code: input.invite_code.clone(),
691
+
invite_code: if is_bootstrap {
692
+
None
693
+
} else {
694
+
input.invite_code.clone()
695
+
},
682
696
birthdate_pref,
683
697
};
684
698
+2
-2
crates/tranquil-pds/src/api/server/invite.rs
+2
-2
crates/tranquil-pds/src/api/server/invite.rs
···
14
14
15
15
const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
16
16
17
-
fn gen_random_token() -> String {
17
+
pub(crate) fn gen_random_token() -> String {
18
18
let mut rng = rand::thread_rng();
19
19
let gen_segment = |rng: &mut rand::rngs::ThreadRng, len: usize| -> String {
20
20
(0..len)
···
24
24
format!("{}-{}", gen_segment(&mut rng, 5), gen_segment(&mut rng, 5))
25
25
}
26
26
27
-
fn gen_invite_code() -> String {
27
+
pub fn gen_invite_code() -> String {
28
28
let hostname = &tranquil_config::get().server.hostname;
29
29
let hostname_prefix = hostname.replace('.', "-");
30
30
format!("{}-{}", hostname_prefix, gen_random_token())
+14
-2
crates/tranquil-pds/src/api/server/passkey_account.rs
+14
-2
crates/tranquil-pds/src/api/server/passkey_account.rs
···
146
146
return ApiError::InvalidEmail.into_response();
147
147
}
148
148
149
-
let _validated_invite_code = if let Some(ref code) = input.invite_code {
149
+
let is_bootstrap = state.bootstrap_invite_code.is_some()
150
+
&& state.user_repo.count_users().await.unwrap_or(1) == 0;
151
+
152
+
let _validated_invite_code = if is_bootstrap {
153
+
match input.invite_code.as_deref() {
154
+
Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => None,
155
+
_ => return ApiError::InvalidInviteCode.into_response(),
156
+
}
157
+
} else if let Some(ref code) = input.invite_code {
150
158
match state.infra_repo.validate_invite_code(code).await {
151
159
Ok(validated) => Some(validated),
152
160
Err(_) => return ApiError::InvalidInviteCode.into_response(),
···
447
455
commit_cid: commit_cid.to_string(),
448
456
repo_rev: rev.as_ref().to_string(),
449
457
genesis_block_cids,
450
-
invite_code: input.invite_code.clone(),
458
+
invite_code: if is_bootstrap {
459
+
None
460
+
} else {
461
+
input.invite_code.clone()
462
+
},
451
463
birthdate_pref,
452
464
};
453
465
+22
-1
crates/tranquil-pds/src/state.rs
+22
-1
crates/tranquil-pds/src/state.rs
···
58
58
pub sso_manager: SsoManager,
59
59
pub webauthn_config: Arc<WebAuthnConfig>,
60
60
pub shutdown: CancellationToken,
61
+
pub bootstrap_invite_code: Option<String>,
61
62
}
62
63
63
64
#[derive(Debug, Clone, Copy)]
···
232
233
.await
233
234
.map_err(|e| format!("Failed to run migrations: {}", e))?;
234
235
235
-
Ok(Self::from_db(db, shutdown).await)
236
+
let bootstrap_invite_code = match (
237
+
cfg.server.invite_code_required,
238
+
sqlx::query_scalar!("SELECT COUNT(*) FROM users")
239
+
.fetch_one(&db)
240
+
.await,
241
+
) {
242
+
(true, Ok(Some(0))) => {
243
+
let code = crate::api::server::invite::gen_invite_code();
244
+
tracing::info!(
245
+
"No users exist and invite codes are required. Bootstrap invite code: {}",
246
+
code
247
+
);
248
+
Some(code)
249
+
}
250
+
_ => None,
251
+
};
252
+
253
+
let mut state = Self::from_db(db, shutdown).await;
254
+
state.bootstrap_invite_code = bootstrap_invite_code;
255
+
Ok(state)
236
256
}
237
257
238
258
pub async fn from_db(db: PgPool, shutdown: CancellationToken) -> Self {
···
285
305
sso_manager,
286
306
webauthn_config,
287
307
shutdown,
308
+
bootstrap_invite_code: None,
288
309
}
289
310
}
290
311