mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-27 20:54:19 +00:00
299 lines
8.6 KiB
Rust
299 lines
8.6 KiB
Rust
use 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 }))
|
|
}
|