invite: newtype InviteCode for invite endpoints + store

Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
Lewis
2026-07-12 08:03:14 +02:00
parent eb1a89dc58
commit fbfa15b0b4
19 changed files with 201 additions and 143 deletions
@@ -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<String> = invite_codes.iter().map(|ic| ic.code.clone()).collect();
let codes: Vec<InviteCode> = 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<InviteCodeInfo> {
async fn get_invite_code_info(state: &AppState, code: &InviteCode) -> Option<InviteCodeInfo> {
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<String> = all_invite_codes
let all_codes: Vec<InviteCode> = 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<uuid::Uuid, String> = state
let invited_by_map: HashMap<uuid::Uuid, InviteCode> = 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<uuid::Uuid, Vec<InviteCodeInfo>>,
HashMap<String, InviteCodeInfo>,
HashMap<InviteCode, InviteCodeInfo>,
) = all_invite_codes.into_iter().fold(
(HashMap::new(), HashMap::new()),
|(mut by_user, mut by_code), (user_id, ic)| {
+10 -10
View File
@@ -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<Vec<String>>,
pub codes: Option<Vec<InviteCode>>,
pub accounts: Option<Vec<Did>>,
}
@@ -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<String>,
pub cursor: Option<InviteCode>,
pub codes: Vec<InviteCodeInfo>,
}
@@ -95,7 +95,7 @@ pub async fn get_invite_codes(
.log_db_err("fetching invite codes")?;
let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect();
let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect();
let code_values: Vec<InviteCode> = codes_rows.iter().map(|r| r.code.clone()).collect();
let creator_dids: std::collections::HashMap<uuid::Uuid, Did> = 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 {
+1 -1
View File
@@ -76,7 +76,7 @@ pub async fn resolve_repo_user_id(
pub fn group_invite_uses_by_code<U, F>(
uses: Vec<tranquil_db_traits::InviteCodeUse>,
map_use: F,
) -> HashMap<String, Vec<U>>
) -> HashMap<tranquil_types::InviteCode, Vec<U>>
where
F: Fn(tranquil_db_traits::InviteCodeUse) -> U,
{
+2 -1
View File
@@ -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<String> = (0..code_count).map(|_| gen_invite_code()).collect();
let codes: Vec<InviteCodeValue> = (0..code_count).map(|_| gen_invite_code()).collect();
infra_repo
.create_invite_codes_batch(&codes, use_count, admin_user_id, Some(&account))
.await
+22 -13
View File
@@ -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<Did>,
@@ -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<Handle>,
pub used_at: DateTime<Utc>,
@@ -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<bool>,
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<bool, DbError>;
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<Option<i32>, DbError>;
async fn get_invite_code_available_uses(
&self,
code: &InviteCode,
) -> Result<Option<i32>, DbError>;
async fn validate_invite_code<'a>(
&self,
code: &'a str,
code: &'a InviteCode,
) -> Result<ValidatedInviteCode<'a>, InviteCodeError>;
async fn get_invite_codes_for_account(
@@ -273,9 +276,9 @@ pub trait InfraRepository: Send + Sync {
for_account: &Did,
) -> Result<Vec<InviteCodeInfo>, DbError>;
async fn get_invite_code_uses(&self, code: &str) -> Result<Vec<InviteCodeUse>, DbError>;
async fn get_invite_code_uses(&self, code: &InviteCode) -> Result<Vec<InviteCodeUse>, 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<Vec<InviteCodeUse>, DbError>;
async fn get_invites_created_by_user(
@@ -298,14 +301,20 @@ pub trait InfraRepository: Send + Sync {
user_id: Uuid,
) -> Result<Vec<InviteCodeInfo>, DbError>;
async fn get_invite_code_info(&self, code: &str) -> Result<Option<InviteCodeInfo>, DbError>;
async fn get_invite_code_info(
&self,
code: &InviteCode,
) -> Result<Option<InviteCodeInfo>, DbError>;
async fn get_invite_codes_by_users(
&self,
user_ids: &[Uuid],
) -> Result<Vec<(Uuid, InviteCodeInfo)>, DbError>;
async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result<Option<String>, DbError>;
async fn get_invite_code_used_by_user(
&self,
user_id: Uuid,
) -> Result<Option<InviteCode>, 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<Vec<(Uuid, String)>, DbError>;
) -> Result<Vec<(Uuid, InviteCode)>, DbError>;
async fn get_deletion_request_by_did(
&self,
+5 -9
View File
@@ -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
}
}
+3 -3
View File
@@ -1010,7 +1010,7 @@ pub struct CreatePasswordAccountInput {
pub commit_cid: CidLink,
pub repo_rev: Tid,
pub genesis_block_cids: Vec<Vec<u8>>,
pub invite_code: Option<String>,
pub invite_code: Option<InviteCode>,
pub birthdate_pref: Option<serde_json::Value>,
}
@@ -1062,7 +1062,7 @@ pub struct CreatePasskeyAccountInput {
pub commit_cid: CidLink,
pub repo_rev: Tid,
pub genesis_block_cids: Vec<Vec<u8>>,
pub invite_code: Option<String>,
pub invite_code: Option<InviteCode>,
pub birthdate_pref: Option<serde_json::Value>,
}
@@ -1080,7 +1080,7 @@ pub struct CreateSsoAccountInput {
pub commit_cid: CidLink,
pub repo_rev: Tid,
pub genesis_block_cids: Vec<Vec<u8>>,
pub invite_code: Option<String>,
pub invite_code: Option<InviteCode>,
pub birthdate_pref: Option<serde_json::Value>,
pub sso_provider: SsoProviderType,
pub sso_provider_user_id: String,
+62 -25
View File
@@ -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<bool, DbError> {
@@ -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<String> = 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<Option<i32>, DbError> {
async fn get_invite_code_available_uses(
&self,
code: &InviteCode,
) -> Result<Option<i32>, 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<ValidatedInviteCode<'a>, 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<Vec<InviteCodeUse>, DbError> {
async fn get_invite_code_uses(&self, code: &InviteCode) -> Result<Vec<InviteCodeUse>, 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<String> = 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<Vec<InviteCodeRow>, DbError> {
fn to_row(
code: String,
available_uses: i32,
disabled: Option<bool>,
created_by_user: Uuid,
created_at: DateTime<Utc>,
) -> 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<Vec<InviteCodeUse>, DbError> {
let code_strs: Vec<String> = 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<Option<InviteCodeInfo>, DbError> {
async fn get_invite_code_info(
&self,
code: &InviteCode,
) -> Result<Option<InviteCodeInfo>, 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<Option<String>, DbError> {
async fn get_invite_code_used_by_user(
&self,
user_id: Uuid,
) -> Result<Option<InviteCode>, 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<Vec<(Uuid, String)>, DbError> {
) -> Result<Vec<(Uuid, InviteCode)>, 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())
}
@@ -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()
@@ -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()
@@ -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()
+10 -5
View File
@@ -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<String>),
Standard(Option<InviteCode>),
}
impl InviteRegistration {
pub fn into_invite_code(self) -> Option<String> {
pub fn into_invite_code(self) -> Option<InviteCode> {
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))
+1 -1
View File
@@ -46,7 +46,7 @@ pub struct AppState {
pub webauthn_config: Arc<WebAuthnConfig>,
pub cross_pds_oauth: Arc<CrossPdsOAuthClient>,
pub shutdown: CancellationToken,
pub bootstrap_invite_code: Option<String>,
pub bootstrap_invite_code: Option<crate::types::InviteCode>,
pub signal_sender: Option<Arc<tranquil_signal::SignalSlot>>,
pub signal_store_provider: Option<Arc<dyn tranquil_signal::SignalStoreProvider>>,
pub eventlog_segments_dir: Option<PathBuf>,
+2 -2
View File
@@ -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 {
@@ -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!(
+1 -1
View File
@@ -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
+27 -12
View File
@@ -1834,7 +1834,7 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
async fn create_invite_code(
&self,
code: &str,
code: &InviteCode,
use_count: i32,
for_account: Option<&Did>,
) -> Result<bool, DbError> {
@@ -1851,7 +1851,7 @@ impl<S: StorageIO + 'static> 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<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
recv(rx).await
}
async fn get_invite_code_available_uses(&self, code: &str) -> Result<Option<i32>, DbError> {
async fn get_invite_code_available_uses(
&self,
code: &InviteCode,
) -> Result<Option<i32>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool.send(MetastoreRequest::Infra(
InfraRequest::GetInviteCodeAvailableUses {
@@ -1882,7 +1885,7 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
async fn validate_invite_code<'a>(
&self,
code: &'a str,
code: &'a InviteCode,
) -> Result<ValidatedInviteCode<'a>, InviteCodeError> {
let (tx, rx) = oneshot::channel();
self.pool
@@ -1909,7 +1912,7 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
recv(rx).await
}
async fn get_invite_code_uses(&self, code: &str) -> Result<Vec<InviteCodeUse>, DbError> {
async fn get_invite_code_uses(&self, code: &InviteCode) -> Result<Vec<InviteCodeUse>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool
.send(MetastoreRequest::Infra(InfraRequest::GetInviteCodeUses {
@@ -1919,7 +1922,7 @@ impl<S: StorageIO + 'static> 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<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
async fn get_invite_code_uses_batch(
&self,
codes: &[String],
codes: &[InviteCode],
) -> Result<Vec<InviteCodeUse>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool.send(MetastoreRequest::Infra(
@@ -1993,7 +1996,10 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
recv(rx).await
}
async fn get_invite_code_info(&self, code: &str) -> Result<Option<InviteCodeInfo>, DbError> {
async fn get_invite_code_info(
&self,
code: &InviteCode,
) -> Result<Option<InviteCodeInfo>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool
.send(MetastoreRequest::Infra(InfraRequest::GetInviteCodeInfo {
@@ -2017,12 +2023,17 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
recv(rx).await
}
async fn get_invite_code_used_by_user(&self, user_id: Uuid) -> Result<Option<String>, DbError> {
async fn get_invite_code_used_by_user(
&self,
user_id: Uuid,
) -> Result<Option<InviteCode>, 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<String>| code.map(InviteCode::from))
}
async fn delete_invite_code_uses_by_user(&self, user_id: Uuid) -> Result<(), DbError> {
@@ -2382,7 +2393,7 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
async fn get_invite_code_uses_by_users(
&self,
user_ids: &[Uuid],
) -> Result<Vec<(Uuid, String)>, DbError> {
) -> Result<Vec<(Uuid, InviteCode)>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool.send(MetastoreRequest::Infra(
InfraRequest::GetInviteCodeUsesByUsers {
@@ -2390,7 +2401,11 @@ impl<S: StorageIO + 'static> 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(
+28 -32
View File
@@ -50,7 +50,7 @@ use crate::metastore::Metastore;
type Tx<T> = oneshot::Sender<Result<T, DbError>>;
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<T>(
infra: &InfraOps,
code: Option<&str>,
code: Option<&InviteCode>,
created: Result<T, CreateAccountError>,
after: impl FnOnce(&T) -> Result<Uuid, CreateAccountError>,
) -> Result<T, CreateAccountError> {
@@ -1828,24 +1828,24 @@ pub enum InfraRequest {
tx: Tx<()>,
},
CreateInviteCode {
code: String,
code: InviteCode,
use_count: i32,
for_account: Option<Did>,
tx: Tx<bool>,
},
CreateInviteCodesBatch {
codes: Vec<String>,
codes: Vec<InviteCode>,
use_count: i32,
created_by_user: Uuid,
for_account: Option<Did>,
tx: Tx<()>,
},
GetInviteCodeAvailableUses {
code: String,
code: InviteCode,
tx: Tx<Option<i32>>,
},
ValidateInviteCode {
code: String,
code: InviteCode,
tx: oneshot::Sender<Result<(), InviteCodeError>>,
},
GetInviteCodesForAccount {
@@ -5777,7 +5777,7 @@ fn dispatch_user<S: StorageIO + 'static>(state: &HandlerState<S>, 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<S: StorageIO + 'static>(state: &HandlerState<S>, 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<S: StorageIO + 'static>(state: &HandlerState<S>, 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)
));
}
@@ -118,7 +118,7 @@ impl InfraOps {
fn value_to_invite_info(&self, v: &InviteCodeValue) -> Result<InviteCodeInfo, MetastoreError> {
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<bool, MetastoreError> {
@@ -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<Option<i32>, MetastoreError> {
let key = invite_code_key(code);
let val: Option<InviteCodeValue> = point_lookup(
@@ -432,7 +432,7 @@ impl InfraOps {
pub fn validate_invite_code<'a>(
&self,
code: &'a str,
code: &'a InviteCode,
) -> Result<ValidatedInviteCode<'a>, InviteCodeError> {
let key = invite_code_key(code);
let val: Option<InviteCodeValue> = 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,