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}