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, pub indexed_at: String, #[serde(skip_serializing_if = "Option::is_none")] pub invite_note: Option, pub invites_disabled: bool, #[serde(skip_serializing_if = "Option::is_none")] pub email_confirmed_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub deactivated_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub invited_by: Option, #[serde(skip_serializing_if = "Option::is_none")] pub invites: Option>, } #[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, #[serde(skip_serializing_if = "Option::is_none")] pub created_by: Option, pub created_at: String, pub uses: Vec, } #[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, } pub async fn get_account_info( State(state): State, _auth: Auth, Query(params): Query, ) -> Result, 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 { 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> { 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 = 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 = 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 { 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, _auth: Auth, RawQuery(raw_query): RawQuery, ) -> Result, ApiError> { let dids: Vec = 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 = 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 = 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 = 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 = 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>, HashMap, ) = 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 = 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 })) }