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::<_, (String, String, Option<String>, chrono::DateTime<chrono::Utc>, bool, Option<chrono::DateTime<chrono::Utc>>)>( 58 r#" 59 SELECT did, handle, email, created_at, email_verified, deactivated_at 60 FROM users 61 WHERE did > $1 AND ($2::text IS NULL OR handle ILIKE $2) 62 ORDER BY did ASC 63 LIMIT $3 64 "#, 65 ) 66 .bind(cursor_did) 67 .bind(&handle_filter) 68 .bind(limit + 1) 69 .fetch_all(&state.db) 70 .await; 71 match result { 72 Ok(rows) => { 73 let has_more = rows.len() > limit as usize; 74 let accounts: Vec<AccountView> = rows 75 .into_iter() 76 .take(limit as usize) 77 .map(|(did, handle, email, created_at, email_verified, deactivated_at)| AccountView { 78 did: did.clone(), 79 handle, 80 email, 81 indexed_at: created_at.to_rfc3339(), 82 email_verified_at: if email_verified { 83 Some(created_at.to_rfc3339()) 84 } else { 85 None 86 }, 87 deactivated_at: deactivated_at.map(|dt| dt.to_rfc3339()), 88 invites_disabled: None, 89 }) 90 .collect(); 91 let next_cursor = if has_more { 92 accounts.last().map(|a| a.did.clone()) 93 } else { 94 None 95 }; 96 ( 97 StatusCode::OK, 98 Json(SearchAccountsOutput { 99 cursor: next_cursor, 100 accounts, 101 }), 102 ) 103 .into_response() 104 } 105 Err(e) => { 106 error!("DB error in search_accounts: {:?}", e); 107 ( 108 StatusCode::INTERNAL_SERVER_ERROR, 109 Json(json!({"error": "InternalError"})), 110 ) 111 .into_response() 112 } 113 } 114}