this repo has no description
at main 4.2 kB view raw
1use crate::api::error::ApiError; 2use crate::auth::BearerAuthAdmin; 3use crate::state::AppState; 4use crate::types::{Did, Handle}; 5use axum::{ 6 Json, 7 extract::{Query, State}, 8 http::StatusCode, 9 response::{IntoResponse, Response}, 10}; 11use serde::{Deserialize, Serialize}; 12use tracing::error; 13 14#[derive(Deserialize)] 15pub struct SearchAccountsParams { 16 pub email: Option<String>, 17 pub handle: Option<String>, 18 pub cursor: Option<String>, 19 #[serde(default = "default_limit")] 20 pub limit: i64, 21} 22 23fn default_limit() -> i64 { 24 50 25} 26 27#[derive(Serialize)] 28#[serde(rename_all = "camelCase")] 29pub struct AccountView { 30 pub did: Did, 31 pub handle: Handle, 32 #[serde(skip_serializing_if = "Option::is_none")] 33 pub email: Option<String>, 34 pub indexed_at: String, 35 #[serde(skip_serializing_if = "Option::is_none")] 36 pub email_confirmed_at: Option<String>, 37 #[serde(skip_serializing_if = "Option::is_none")] 38 pub deactivated_at: Option<String>, 39 #[serde(skip_serializing_if = "Option::is_none")] 40 pub invites_disabled: Option<bool>, 41} 42 43#[derive(Serialize)] 44#[serde(rename_all = "camelCase")] 45pub struct SearchAccountsOutput { 46 #[serde(skip_serializing_if = "Option::is_none")] 47 pub cursor: Option<String>, 48 pub accounts: Vec<AccountView>, 49} 50 51pub async fn search_accounts( 52 State(state): State<AppState>, 53 _auth: BearerAuthAdmin, 54 Query(params): Query<SearchAccountsParams>, 55) -> Response { 56 let limit = params.limit.clamp(1, 100); 57 let cursor_did = params.cursor.as_deref().unwrap_or(""); 58 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 59 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 60 let result = sqlx::query_as::< 61 _, 62 ( 63 String, 64 String, 65 Option<String>, 66 chrono::DateTime<chrono::Utc>, 67 bool, 68 Option<chrono::DateTime<chrono::Utc>>, 69 Option<bool>, 70 ), 71 >( 72 r#" 73 SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled 74 FROM users 75 WHERE did > $1 76 AND ($2::text IS NULL OR email ILIKE $2) 77 AND ($3::text IS NULL OR handle ILIKE $3) 78 ORDER BY did ASC 79 LIMIT $4 80 "#, 81 ) 82 .bind(cursor_did) 83 .bind(&email_filter) 84 .bind(&handle_filter) 85 .bind(limit + 1) 86 .fetch_all(&state.db) 87 .await; 88 match result { 89 Ok(rows) => { 90 let has_more = rows.len() > limit as usize; 91 let accounts: Vec<AccountView> = rows 92 .into_iter() 93 .take(limit as usize) 94 .map( 95 |( 96 did, 97 handle, 98 email, 99 created_at, 100 email_verified, 101 deactivated_at, 102 invites_disabled, 103 )| { 104 AccountView { 105 did: did.clone().into(), 106 handle: handle.into(), 107 email, 108 indexed_at: created_at.to_rfc3339(), 109 email_confirmed_at: if email_verified { 110 Some(created_at.to_rfc3339()) 111 } else { 112 None 113 }, 114 deactivated_at: deactivated_at.map(|dt| dt.to_rfc3339()), 115 invites_disabled, 116 } 117 }, 118 ) 119 .collect(); 120 let next_cursor = if has_more { 121 accounts.last().map(|a| a.did.to_string()) 122 } else { 123 None 124 }; 125 ( 126 StatusCode::OK, 127 Json(SearchAccountsOutput { 128 cursor: next_cursor, 129 accounts, 130 }), 131 ) 132 .into_response() 133 } 134 Err(e) => { 135 error!("DB error in search_accounts: {:?}", e); 136 ApiError::InternalError(None).into_response() 137 } 138 } 139}