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}