Something went wrong. Try again.
Our Personal Data Server from scratch!
Something went wrong. Try again.
6.3 kB · 218 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219use crate::common;use axum::{ Json, extract::{Query, State},};use serde::{Deserialize, Serialize};use tracing::error;use tranquil_db_traits::InviteCodeSortOrder;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<InviteCode>>, pub accounts: Option<Vec<Did>>,}
pub async fn disable_invite_codes( State(state): State<AppState>, _auth: Auth<Admin>, Json(input): Json<DisableInviteCodesInput>,) -> Result<Json<EmptyResponse>, ApiError> { if let Some(codes) = &input.codes && let Err(e) = state.repos.infra.disable_invite_codes_by_code(codes).await { error!("DB error disabling invite codes: {:?}", e); } if let Some(accounts) = &input.accounts && let Err(e) = state .repos .infra .disable_invite_codes_by_account(accounts) .await { error!("DB error disabling invite codes by account: {:?}", e); } Ok(Json(EmptyResponse {}))}
#[derive(Deserialize)]pub struct GetInviteCodesParams { pub sort: Option<String>, pub limit: Option<i64>, pub cursor: Option<String>,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct InviteCodeInfo { pub code: InviteCode, pub available: i32, pub disabled: bool, pub for_account: String, pub created_by: String, pub created_at: String, pub uses: Vec<InviteCodeUseInfo>,}
#[derive(Clone, Serialize)]#[serde(rename_all = "camelCase")]pub struct InviteCodeUseInfo { pub used_by: String, pub used_at: String,}
#[derive(Serialize)]pub struct GetInviteCodesOutput { #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option<InviteCode>, pub codes: Vec<InviteCodeInfo>,}
pub async fn get_invite_codes( State(state): State<AppState>, _auth: Auth<Admin>, Query(params): Query<GetInviteCodesParams>,) -> Result<Json<GetInviteCodesOutput>, ApiError> { let limit = params.limit.unwrap_or(100).clamp(1, 500); let sort_order = match params.sort.as_deref() { Some("usage") => InviteCodeSortOrder::Usage, _ => InviteCodeSortOrder::Recent, };
let codes_rows = state .repos .infra .list_invite_codes(params.cursor.as_deref(), limit, sort_order) .await .log_db_err("fetching invite codes")?;
let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).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 .infra .get_user_dids_by_ids(&user_ids) .await .unwrap_or_default() .into_iter() .collect();
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_values) .await .unwrap_or_default(), |u| InviteCodeUseInfo { used_by: u.used_by_did.to_string(), used_at: u.used_at.to_rfc3339(), }, ) };
let codes: Vec<InviteCodeInfo> = codes_rows .iter() .map(|r| { let creator_did = creator_dids .get(&r.created_by_user) .map(|d| d.to_string()) .unwrap_or_else(|| "unknown".to_string()); InviteCodeInfo { code: r.code.clone(), available: r.available_uses, disabled: r.state().is_disabled(), for_account: creator_did.clone(), created_by: creator_did, created_at: r.created_at.to_rfc3339(), uses: uses_by_code.get(&r.code).cloned().unwrap_or_default(), } }) .collect();
let next_cursor = if codes_rows.len() == usize::try_from(limit).unwrap_or(0) { codes_rows.last().map(|r| r.code.clone()) } else { None }; Ok(Json(GetInviteCodesOutput { cursor: next_cursor, codes, }))}
#[derive(Deserialize)]pub struct DisableAccountInvitesInput { pub account: String,}
pub async fn disable_account_invites( State(state): State<AppState>, _auth: Auth<Admin>, Json(input): Json<DisableAccountInvitesInput>,) -> Result<Json<EmptyResponse>, ApiError> { let account = input.account.trim(); if account.is_empty() { return Err(ApiError::InvalidRequest("account is required".into())); } let account_did: Did = account .parse() .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
match state .repos .user .set_invites_disabled(&account_did, true) .await { Ok(true) => Ok(Json(EmptyResponse {})), Ok(false) => Err(ApiError::AccountNotFound), Err(e) => { error!("DB error disabling account invites: {:?}", e); Err(ApiError::InternalError(None)) } }}
#[derive(Deserialize)]pub struct EnableAccountInvitesInput { pub account: String,}
pub async fn enable_account_invites( State(state): State<AppState>, _auth: Auth<Admin>, Json(input): Json<EnableAccountInvitesInput>,) -> Result<Json<EmptyResponse>, ApiError> { let account = input.account.trim(); if account.is_empty() { return Err(ApiError::InvalidRequest("account is required".into())); } let account_did: Did = account .parse() .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
match state .repos .user .set_invites_disabled(&account_did, false) .await { Ok(true) => Ok(Json(EmptyResponse {})), Ok(false) => Err(ApiError::AccountNotFound), Err(e) => { error!("DB error enabling account invites: {:?}", e); Err(ApiError::InternalError(None)) } }}