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_confirmed_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_confirmed, 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_confirmed, deactivated_at)| AccountView {
78 did: did.clone(),
79 handle,
80 email,
81 indexed_at: created_at.to_rfc3339(),
82 email_confirmed_at: if email_confirmed {
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}