Something went wrong. Try again.
A downstream fork of Tranquil PDS for use in margin.cafe
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299use crate::common;use axum::{ Json, extract::{Query, RawQuery, State},};use serde::{Deserialize, Serialize};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, InviteCode};
#[derive(Deserialize)]pub struct GetAccountInfoParams { pub did: Did,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct AccountInfo { pub did: Did, pub handle: Handle, #[serde(skip_serializing_if = "Option::is_none")] pub email: Option<String>, pub indexed_at: String, #[serde(skip_serializing_if = "Option::is_none")] pub invite_note: Option<String>, pub invites_disabled: bool, #[serde(skip_serializing_if = "Option::is_none")] pub email_confirmed_at: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub deactivated_at: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub invited_by: Option<InviteCodeInfo>, #[serde(skip_serializing_if = "Option::is_none")] pub invites: Option<Vec<InviteCodeInfo>>,}
#[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]pub struct InviteCodeInfo { pub code: InviteCode, pub available: i32, pub disabled: bool, #[serde(skip_serializing_if = "Option::is_none")] pub for_account: Option<Did>, #[serde(skip_serializing_if = "Option::is_none")] pub created_by: Option<Did>, pub created_at: String, pub uses: Vec<InviteCodeUseInfo>,}
#[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]pub struct InviteCodeUseInfo { pub used_by: Did, pub used_at: String,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct GetAccountInfosOutput { pub infos: Vec<AccountInfo>,}
pub async fn get_account_info( State(state): State<AppState>, _auth: Auth<Admin>, Query(params): Query<GetAccountInfoParams>,) -> Result<Json<AccountInfo>, ApiError> { let account = state .repos .infra .get_admin_account_info_by_did(¶ms.did) .await .log_db_err("in get_account_info")? .ok_or(ApiError::AccountNotFound)?;
let invited_by = get_invited_by(&state, account.id).await; let invites = get_invites_for_user(&state, account.id).await;
Ok(Json(AccountInfo { did: account.did, handle: account.handle, email: account.email, indexed_at: account.created_at.to_rfc3339(), invite_note: None, invites_disabled: account.invites_disabled, email_confirmed_at: if account.email_verified { Some(account.created_at.to_rfc3339()) } else { None }, deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()), invited_by, invites, }))}
async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { let code = state .repos .infra .get_invite_code_used_by_user(user_id) .await .ok()??;
get_invite_code_info(state, &code).await}
async fn get_invites_for_user( state: &AppState, user_id: uuid::Uuid,) -> Option<Vec<InviteCodeInfo>> { let invite_codes = state .repos .infra .get_invites_created_by_user(user_id) .await .ok()?;
if invite_codes.is_empty() { return None; }
let codes: Vec<InviteCode> = invite_codes.iter().map(|ic| ic.code.clone()).collect();
let uses = state .repos .infra .get_invite_code_uses_batch(&codes) .await .ok()?;
let uses_by_code = common::group_invite_uses_by_code(uses, |u| InviteCodeUseInfo { used_by: u.used_by_did, used_at: u.used_at.to_rfc3339(), });
let invites: Vec<InviteCodeInfo> = invite_codes .into_iter() .map(|ic| InviteCodeInfo { code: ic.code.clone(), available: ic.available_uses, disabled: ic.state.is_disabled(), for_account: ic.for_account, created_by: ic.created_by, created_at: ic.created_at.to_rfc3339(), uses: uses_by_code.get(&ic.code).cloned().unwrap_or_default(), }) .collect();
if invites.is_empty() { None } else { Some(invites) }}
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 .repos .infra .get_invite_code_uses(code) .await .ok() .unwrap_or_default();
Some(InviteCodeInfo { code: info.code, available: info.available_uses, disabled: info.state.is_disabled(), for_account: info.for_account, created_by: info.created_by, created_at: info.created_at.to_rfc3339(), uses: uses .into_iter() .map(|u| InviteCodeUseInfo { used_by: u.used_by_did, used_at: u.used_at.to_rfc3339(), }) .collect(), })}
pub async fn get_account_infos( State(state): State<AppState>, _auth: Auth<Admin>, RawQuery(raw_query): RawQuery,) -> Result<Json<GetAccountInfosOutput>, ApiError> { let dids: Vec<String> = tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids") .into_iter() .filter(|d| !d.is_empty()) .collect();
if dids.is_empty() { return Err(ApiError::InvalidRequest("dids is required".into())); }
let dids: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); let accounts = state .repos .infra .get_admin_account_infos_by_dids(&dids) .await .log_db_err("fetching account infos")?;
let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect();
let all_invite_codes = state .repos .infra .get_invite_codes_by_users(&user_ids) .await .unwrap_or_default();
let all_codes: Vec<InviteCode> = all_invite_codes .iter() .map(|(_, c)| c.code.clone()) .collect();
let all_invite_uses = if !all_codes.is_empty() { state .repos .infra .get_invite_code_uses_batch(&all_codes) .await .unwrap_or_default() } else { Vec::new() };
let invited_by_map: HashMap<uuid::Uuid, InviteCode> = state .repos .infra .get_invite_code_uses_by_users(&user_ids) .await .unwrap_or_default() .into_iter() .collect();
let uses_by_code = common::group_invite_uses_by_code(all_invite_uses, |u| InviteCodeUseInfo { used_by: u.used_by_did, used_at: u.used_at.to_rfc3339(), });
let (codes_by_user, code_info_map): ( HashMap<uuid::Uuid, Vec<InviteCodeInfo>>, HashMap<InviteCode, InviteCodeInfo>, ) = all_invite_codes.into_iter().fold( (HashMap::new(), HashMap::new()), |(mut by_user, mut by_code), (user_id, ic)| { let info = InviteCodeInfo { code: ic.code.clone(), available: ic.available_uses, disabled: ic.state.is_disabled(), for_account: ic.for_account, created_by: ic.created_by, created_at: ic.created_at.to_rfc3339(), uses: uses_by_code.get(&ic.code).cloned().unwrap_or_default(), }; by_code.insert(ic.code.clone(), info.clone()); by_user.entry(user_id).or_default().push(info); (by_user, by_code) }, );
let infos: Vec<AccountInfo> = accounts .into_iter() .map(|account| { let invited_by = invited_by_map .get(&account.id) .and_then(|code| code_info_map.get(code).cloned()); let invites = codes_by_user.get(&account.id).cloned(); AccountInfo { did: account.did, handle: account.handle, email: account.email, indexed_at: account.created_at.to_rfc3339(), invite_note: None, invites_disabled: account.invites_disabled, email_confirmed_at: if account.email_verified { Some(account.created_at.to_rfc3339()) } else { None }, deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()), invited_by, invites, } }) .collect();
Ok(Json(GetAccountInfosOutput { infos }))}