this repo has no description
1use crate::api::error::ApiError;
2use crate::auth::BearerAuthAdmin;
3use crate::state::AppState;
4use crate::types::{Did, Handle};
5use axum::{
6 Json,
7 extract::{Query, State},
8 http::StatusCode,
9 response::{IntoResponse, Response},
10};
11use serde::{Deserialize, Serialize};
12use tracing::error;
13
14#[derive(Deserialize)]
15pub struct SearchAccountsParams {
16 pub email: Option<String>,
17 pub handle: Option<String>,
18 pub cursor: Option<String>,
19 #[serde(default = "default_limit")]
20 pub limit: i64,
21}
22
23fn default_limit() -> i64 {
24 50
25}
26
27#[derive(Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct AccountView {
30 pub did: Did,
31 pub handle: Handle,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub email: Option<String>,
34 pub indexed_at: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub email_confirmed_at: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub deactivated_at: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub invites_disabled: Option<bool>,
41}
42
43#[derive(Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct SearchAccountsOutput {
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub cursor: Option<String>,
48 pub accounts: Vec<AccountView>,
49}
50
51pub async fn search_accounts(
52 State(state): State<AppState>,
53 _auth: BearerAuthAdmin,
54 Query(params): Query<SearchAccountsParams>,
55) -> Response {
56 let limit = params.limit.clamp(1, 100);
57 let cursor_did = params.cursor.as_deref().unwrap_or("");
58 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e));
59 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
60 let result = sqlx::query_as::<
61 _,
62 (
63 String,
64 String,
65 Option<String>,
66 chrono::DateTime<chrono::Utc>,
67 bool,
68 Option<chrono::DateTime<chrono::Utc>>,
69 Option<bool>,
70 ),
71 >(
72 r#"
73 SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled
74 FROM users
75 WHERE did > $1
76 AND ($2::text IS NULL OR email ILIKE $2)
77 AND ($3::text IS NULL OR handle ILIKE $3)
78 ORDER BY did ASC
79 LIMIT $4
80 "#,
81 )
82 .bind(cursor_did)
83 .bind(&email_filter)
84 .bind(&handle_filter)
85 .bind(limit + 1)
86 .fetch_all(&state.db)
87 .await;
88 match result {
89 Ok(rows) => {
90 let has_more = rows.len() > limit as usize;
91 let accounts: Vec<AccountView> = rows
92 .into_iter()
93 .take(limit as usize)
94 .map(
95 |(
96 did,
97 handle,
98 email,
99 created_at,
100 email_verified,
101 deactivated_at,
102 invites_disabled,
103 )| {
104 AccountView {
105 did: did.clone().into(),
106 handle: handle.into(),
107 email,
108 indexed_at: created_at.to_rfc3339(),
109 email_confirmed_at: if email_verified {
110 Some(created_at.to_rfc3339())
111 } else {
112 None
113 },
114 deactivated_at: deactivated_at.map(|dt| dt.to_rfc3339()),
115 invites_disabled,
116 }
117 },
118 )
119 .collect();
120 let next_cursor = if has_more {
121 accounts.last().map(|a| a.did.to_string())
122 } else {
123 None
124 };
125 (
126 StatusCode::OK,
127 Json(SearchAccountsOutput {
128 cursor: next_cursor,
129 accounts,
130 }),
131 )
132 .into_response()
133 }
134 Err(e) => {
135 error!("DB error in search_accounts: {:?}", e);
136 ApiError::InternalError(None).into_response()
137 }
138 }
139}