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