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 |(
95 did,
96 handle,
97 email,
98 created_at,
99 email_verified,
100 deactivated_at,
101 invites_disabled,
102 )| {
103 AccountView {
104 did: did.clone(),
105 handle,
106 email,
107 indexed_at: created_at.to_rfc3339(),
108 email_confirmed_at: if email_verified {
109 Some(created_at.to_rfc3339())
110 } else {
111 None
112 },
113 deactivated_at: deactivated_at.map(|dt| dt.to_rfc3339()),
114 invites_disabled,
115 }
116 },
117 )
118 .collect();
119 let next_cursor = if has_more {
120 accounts.last().map(|a| a.did.clone())
121 } else {
122 None
123 };
124 (
125 StatusCode::OK,
126 Json(SearchAccountsOutput {
127 cursor: next_cursor,
128 accounts,
129 }),
130 )
131 .into_response()
132 }
133 Err(e) => {
134 error!("DB error in search_accounts: {:?}", e);
135 (
136 StatusCode::INTERNAL_SERVER_ERROR,
137 Json(json!({"error": "InternalError"})),
138 )
139 .into_response()
140 }
141 }
142}