use crate::api::error::ApiError; use crate::auth::BearerAuthAdmin; use crate::state::AppState; use crate::types::{Did, Handle}; use axum::{ Json, extract::{Query, State}, http::StatusCode, response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use tracing::error; #[derive(Deserialize)] pub struct SearchAccountsParams { pub email: Option, pub handle: Option, pub cursor: Option, #[serde(default = "default_limit")] pub limit: i64, } fn default_limit() -> i64 { 50 } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountView { pub did: Did, pub handle: Handle, #[serde(skip_serializing_if = "Option::is_none")] pub email: Option, pub indexed_at: String, #[serde(skip_serializing_if = "Option::is_none")] pub email_confirmed_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub deactivated_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub invites_disabled: Option, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SearchAccountsOutput { #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, pub accounts: Vec, } pub async fn search_accounts( State(state): State, _auth: BearerAuthAdmin, Query(params): Query, ) -> Response { let limit = params.limit.clamp(1, 100); let cursor_did = params.cursor.as_deref().unwrap_or(""); let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); let result = sqlx::query_as::< _, ( String, String, Option, chrono::DateTime, bool, Option>, Option, ), >( r#" SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled FROM users WHERE did > $1 AND ($2::text IS NULL OR email ILIKE $2) AND ($3::text IS NULL OR handle ILIKE $3) ORDER BY did ASC LIMIT $4 "#, ) .bind(cursor_did) .bind(&email_filter) .bind(&handle_filter) .bind(limit + 1) .fetch_all(&state.db) .await; match result { Ok(rows) => { let has_more = rows.len() > limit as usize; let accounts: Vec = rows .into_iter() .take(limit as usize) .map( |( did, handle, email, created_at, email_verified, deactivated_at, invites_disabled, )| { AccountView { did: did.clone().into(), handle: handle.into(), email, indexed_at: created_at.to_rfc3339(), email_confirmed_at: if email_verified { Some(created_at.to_rfc3339()) } else { None }, deactivated_at: deactivated_at.map(|dt| dt.to_rfc3339()), invites_disabled, } }, ) .collect(); let next_cursor = if has_more { accounts.last().map(|a| a.did.to_string()) } else { None }; ( StatusCode::OK, Json(SearchAccountsOutput { cursor: next_cursor, accounts, }), ) .into_response() } Err(e) => { error!("DB error in search_accounts: {:?}", e); ApiError::InternalError(None).into_response() } } }