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, RawQuery, State},
8 http::StatusCode,
9 response::{IntoResponse, Response},
10};
11use serde::{Deserialize, Serialize};
12use tracing::error;
13
14#[derive(Deserialize)]
15pub struct GetAccountInfoParams {
16 pub did: Did,
17}
18
19#[derive(Serialize)]
20#[serde(rename_all = "camelCase")]
21pub struct AccountInfo {
22 pub did: Did,
23 pub handle: Handle,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub email: Option<String>,
26 pub indexed_at: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub invite_note: Option<String>,
29 pub invites_disabled: bool,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub email_confirmed_at: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub deactivated_at: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub invited_by: Option<InviteCodeInfo>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub invites: Option<Vec<InviteCodeInfo>>,
38}
39
40#[derive(Serialize, Clone)]
41#[serde(rename_all = "camelCase")]
42pub struct InviteCodeInfo {
43 pub code: String,
44 pub available: i32,
45 pub disabled: bool,
46 pub for_account: Did,
47 pub created_by: Did,
48 pub created_at: String,
49 pub uses: Vec<InviteCodeUseInfo>,
50}
51
52#[derive(Serialize, Clone)]
53#[serde(rename_all = "camelCase")]
54pub struct InviteCodeUseInfo {
55 pub used_by: Did,
56 pub used_at: String,
57}
58
59#[derive(Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct GetAccountInfosOutput {
62 pub infos: Vec<AccountInfo>,
63}
64
65pub async fn get_account_info(
66 State(state): State<AppState>,
67 _auth: BearerAuthAdmin,
68 Query(params): Query<GetAccountInfoParams>,
69) -> Response {
70 let result = sqlx::query!(
71 r#"
72 SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
73 FROM users
74 WHERE did = $1
75 "#,
76 params.did.as_str()
77 )
78 .fetch_optional(&state.db)
79 .await;
80 match result {
81 Ok(Some(row)) => {
82 let invited_by = get_invited_by(&state.db, row.id).await;
83 let invites = get_invites_for_user(&state.db, row.id).await;
84 (
85 StatusCode::OK,
86 Json(AccountInfo {
87 did: row.did.into(),
88 handle: row.handle.into(),
89 email: row.email,
90 indexed_at: row.created_at.to_rfc3339(),
91 invite_note: None,
92 invites_disabled: row.invites_disabled.unwrap_or(false),
93 email_confirmed_at: if row.email_verified {
94 Some(row.created_at.to_rfc3339())
95 } else {
96 None
97 },
98 deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
99 invited_by,
100 invites,
101 }),
102 )
103 .into_response()
104 }
105 Ok(None) => ApiError::AccountNotFound.into_response(),
106 Err(e) => {
107 error!("DB error in get_account_info: {:?}", e);
108 ApiError::InternalError(None).into_response()
109 }
110 }
111}
112
113async fn get_invited_by(db: &sqlx::PgPool, user_id: uuid::Uuid) -> Option<InviteCodeInfo> {
114 let use_row = sqlx::query!(
115 r#"
116 SELECT icu.code
117 FROM invite_code_uses icu
118 WHERE icu.used_by_user = $1
119 LIMIT 1
120 "#,
121 user_id
122 )
123 .fetch_optional(db)
124 .await
125 .ok()??;
126 get_invite_code_info(db, &use_row.code).await
127}
128
129async fn get_invites_for_user(
130 db: &sqlx::PgPool,
131 user_id: uuid::Uuid,
132) -> Option<Vec<InviteCodeInfo>> {
133 let codes = sqlx::query_scalar!(
134 r#"
135 SELECT code FROM invite_codes WHERE created_by_user = $1
136 "#,
137 user_id
138 )
139 .fetch_all(db)
140 .await
141 .ok()?;
142 if codes.is_empty() {
143 return None;
144 }
145 let mut invites = Vec::new();
146 for code in codes {
147 if let Some(info) = get_invite_code_info(db, &code).await {
148 invites.push(info);
149 }
150 }
151 if invites.is_empty() {
152 None
153 } else {
154 Some(invites)
155 }
156}
157
158async fn get_invite_code_info(db: &sqlx::PgPool, code: &str) -> Option<InviteCodeInfo> {
159 let row = sqlx::query!(
160 r#"
161 SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by
162 FROM invite_codes ic
163 JOIN users u ON ic.created_by_user = u.id
164 WHERE ic.code = $1
165 "#,
166 code
167 )
168 .fetch_optional(db)
169 .await
170 .ok()??;
171 let uses = sqlx::query!(
172 r#"
173 SELECT u.did as used_by, icu.used_at
174 FROM invite_code_uses icu
175 JOIN users u ON icu.used_by_user = u.id
176 WHERE icu.code = $1
177 "#,
178 code
179 )
180 .fetch_all(db)
181 .await
182 .ok()?;
183 Some(InviteCodeInfo {
184 code: row.code,
185 available: row.available_uses,
186 disabled: row.disabled.unwrap_or(false),
187 for_account: row.for_account.into(),
188 created_by: row.created_by.into(),
189 created_at: row.created_at.to_rfc3339(),
190 uses: uses
191 .into_iter()
192 .map(|u| InviteCodeUseInfo {
193 used_by: u.used_by.into(),
194 used_at: u.used_at.to_rfc3339(),
195 })
196 .collect(),
197 })
198}
199
200pub async fn get_account_infos(
201 State(state): State<AppState>,
202 _auth: BearerAuthAdmin,
203 RawQuery(raw_query): RawQuery,
204) -> Response {
205 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
206 .into_iter()
207 .filter(|d| !d.is_empty())
208 .collect();
209 if dids.is_empty() {
210 return ApiError::InvalidRequest("dids is required".into()).into_response();
211 }
212 let users = match sqlx::query!(
213 r#"
214 SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
215 FROM users
216 WHERE did = ANY($1)
217 "#,
218 &dids
219 )
220 .fetch_all(&state.db)
221 .await
222 {
223 Ok(rows) => rows,
224 Err(e) => {
225 error!("Failed to fetch account infos: {:?}", e);
226 return ApiError::InternalError(None).into_response();
227 }
228 };
229
230 let user_ids: Vec<uuid::Uuid> = users.iter().map(|u| u.id).collect();
231
232 let all_invite_codes = sqlx::query!(
233 r#"
234 SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at,
235 ic.created_by_user, u.did as created_by
236 FROM invite_codes ic
237 JOIN users u ON ic.created_by_user = u.id
238 WHERE ic.created_by_user = ANY($1)
239 "#,
240 &user_ids
241 )
242 .fetch_all(&state.db)
243 .await
244 .unwrap_or_default();
245
246 let all_codes: Vec<String> = all_invite_codes.iter().map(|c| c.code.clone()).collect();
247 let all_invite_uses = if !all_codes.is_empty() {
248 sqlx::query!(
249 r#"
250 SELECT icu.code, u.did as used_by, icu.used_at
251 FROM invite_code_uses icu
252 JOIN users u ON icu.used_by_user = u.id
253 WHERE icu.code = ANY($1)
254 "#,
255 &all_codes
256 )
257 .fetch_all(&state.db)
258 .await
259 .unwrap_or_default()
260 } else {
261 Vec::new()
262 };
263
264 let invited_by_map: std::collections::HashMap<uuid::Uuid, String> = sqlx::query!(
265 r#"
266 SELECT icu.used_by_user, icu.code
267 FROM invite_code_uses icu
268 WHERE icu.used_by_user = ANY($1)
269 "#,
270 &user_ids
271 )
272 .fetch_all(&state.db)
273 .await
274 .unwrap_or_default()
275 .into_iter()
276 .map(|r| (r.used_by_user, r.code))
277 .collect();
278
279 let mut uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> =
280 std::collections::HashMap::new();
281 for u in all_invite_uses {
282 uses_by_code
283 .entry(u.code.clone())
284 .or_default()
285 .push(InviteCodeUseInfo {
286 used_by: u.used_by.into(),
287 used_at: u.used_at.to_rfc3339(),
288 });
289 }
290
291 let mut codes_by_user: std::collections::HashMap<uuid::Uuid, Vec<InviteCodeInfo>> =
292 std::collections::HashMap::new();
293 let mut code_info_map: std::collections::HashMap<String, InviteCodeInfo> =
294 std::collections::HashMap::new();
295 for ic in all_invite_codes {
296 let info = InviteCodeInfo {
297 code: ic.code.clone(),
298 available: ic.available_uses,
299 disabled: ic.disabled.unwrap_or(false),
300 for_account: ic.for_account.into(),
301 created_by: ic.created_by.into(),
302 created_at: ic.created_at.to_rfc3339(),
303 uses: uses_by_code.get(&ic.code).cloned().unwrap_or_default(),
304 };
305 code_info_map.insert(ic.code.clone(), info.clone());
306 codes_by_user
307 .entry(ic.created_by_user)
308 .or_default()
309 .push(info);
310 }
311
312 let mut infos = Vec::with_capacity(users.len());
313 for row in users {
314 let invited_by = invited_by_map
315 .get(&row.id)
316 .and_then(|code| code_info_map.get(code).cloned());
317 let invites = codes_by_user.get(&row.id).cloned();
318 infos.push(AccountInfo {
319 did: row.did.into(),
320 handle: row.handle.into(),
321 email: row.email,
322 indexed_at: row.created_at.to_rfc3339(),
323 invite_note: None,
324 invites_disabled: row.invites_disabled.unwrap_or(false),
325 email_confirmed_at: if row.email_verified {
326 Some(row.created_at.to_rfc3339())
327 } else {
328 None
329 },
330 deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
331 invited_by,
332 invites,
333 });
334 }
335 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
336}