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