mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-13 13:44:13 +00:00
invite: newtype InviteCode for invite endpoints + store
Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
@@ -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,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 {
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user