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 invite_codes = sqlx::query!(
134 r#"
135 SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by
136 FROM invite_codes ic
137 JOIN users u ON ic.created_by_user = u.id
138 WHERE ic.created_by_user = $1
139 "#,
140 user_id
141 )
142 .fetch_all(db)
143 .await
144 .ok()?;
145
146 if invite_codes.is_empty() {
147 return None;
148 }
149
150 let code_strings: Vec<String> = invite_codes.iter().map(|ic| ic.code.clone()).collect();
151 let mut uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> =
152 std::collections::HashMap::new();
153 sqlx::query!(
154 r#"
155 SELECT icu.code, u.did as used_by, icu.used_at
156 FROM invite_code_uses icu
157 JOIN users u ON icu.used_by_user = u.id
158 WHERE icu.code = ANY($1)
159 "#,
160 &code_strings
161 )
162 .fetch_all(db)
163 .await
164 .ok()?
165 .into_iter()
166 .for_each(|r| {
167 uses_by_code
168 .entry(r.code)
169 .or_default()
170 .push(InviteCodeUseInfo {
171 used_by: r.used_by.into(),
172 used_at: r.used_at.to_rfc3339(),
173 });
174 });
175
176 let invites: Vec<InviteCodeInfo> = invite_codes
177 .into_iter()
178 .map(|ic| InviteCodeInfo {
179 code: ic.code.clone(),
180 available: ic.available_uses,
181 disabled: ic.disabled.unwrap_or(false),
182 for_account: ic.for_account.into(),
183 created_by: ic.created_by.into(),
184 created_at: ic.created_at.to_rfc3339(),
185 uses: uses_by_code.get(&ic.code).cloned().unwrap_or_default(),
186 })
187 .collect();
188
189 if invites.is_empty() {
190 None
191 } else {
192 Some(invites)
193 }
194}
195
196async fn get_invite_code_info(db: &sqlx::PgPool, code: &str) -> Option<InviteCodeInfo> {
197 let row = sqlx::query!(
198 r#"
199 SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by
200 FROM invite_codes ic
201 JOIN users u ON ic.created_by_user = u.id
202 WHERE ic.code = $1
203 "#,
204 code
205 )
206 .fetch_optional(db)
207 .await
208 .ok()??;
209 let uses = sqlx::query!(
210 r#"
211 SELECT u.did as used_by, icu.used_at
212 FROM invite_code_uses icu
213 JOIN users u ON icu.used_by_user = u.id
214 WHERE icu.code = $1
215 "#,
216 code
217 )
218 .fetch_all(db)
219 .await
220 .ok()?;
221 Some(InviteCodeInfo {
222 code: row.code,
223 available: row.available_uses,
224 disabled: row.disabled.unwrap_or(false),
225 for_account: row.for_account.into(),
226 created_by: row.created_by.into(),
227 created_at: row.created_at.to_rfc3339(),
228 uses: uses
229 .into_iter()
230 .map(|u| InviteCodeUseInfo {
231 used_by: u.used_by.into(),
232 used_at: u.used_at.to_rfc3339(),
233 })
234 .collect(),
235 })
236}
237
238pub async fn get_account_infos(
239 State(state): State<AppState>,
240 _auth: BearerAuthAdmin,
241 RawQuery(raw_query): RawQuery,
242) -> Response {
243 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
244 .into_iter()
245 .filter(|d| !d.is_empty())
246 .collect();
247 if dids.is_empty() {
248 return ApiError::InvalidRequest("dids is required".into()).into_response();
249 }
250 let users = match sqlx::query!(
251 r#"
252 SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
253 FROM users
254 WHERE did = ANY($1)
255 "#,
256 &dids
257 )
258 .fetch_all(&state.db)
259 .await
260 {
261 Ok(rows) => rows,
262 Err(e) => {
263 error!("Failed to fetch account infos: {:?}", e);
264 return ApiError::InternalError(None).into_response();
265 }
266 };
267
268 let user_ids: Vec<uuid::Uuid> = users.iter().map(|u| u.id).collect();
269
270 let all_invite_codes = sqlx::query!(
271 r#"
272 SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at,
273 ic.created_by_user, u.did as created_by
274 FROM invite_codes ic
275 JOIN users u ON ic.created_by_user = u.id
276 WHERE ic.created_by_user = ANY($1)
277 "#,
278 &user_ids
279 )
280 .fetch_all(&state.db)
281 .await
282 .unwrap_or_default();
283
284 let all_codes: Vec<String> = all_invite_codes.iter().map(|c| c.code.clone()).collect();
285 let all_invite_uses = if !all_codes.is_empty() {
286 sqlx::query!(
287 r#"
288 SELECT icu.code, u.did as used_by, icu.used_at
289 FROM invite_code_uses icu
290 JOIN users u ON icu.used_by_user = u.id
291 WHERE icu.code = ANY($1)
292 "#,
293 &all_codes
294 )
295 .fetch_all(&state.db)
296 .await
297 .unwrap_or_default()
298 } else {
299 Vec::new()
300 };
301
302 let invited_by_map: std::collections::HashMap<uuid::Uuid, String> = sqlx::query!(
303 r#"
304 SELECT icu.used_by_user, icu.code
305 FROM invite_code_uses icu
306 WHERE icu.used_by_user = ANY($1)
307 "#,
308 &user_ids
309 )
310 .fetch_all(&state.db)
311 .await
312 .unwrap_or_default()
313 .into_iter()
314 .map(|r| (r.used_by_user, r.code))
315 .collect();
316
317 let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> =
318 all_invite_uses
319 .into_iter()
320 .fold(std::collections::HashMap::new(), |mut acc, u| {
321 acc.entry(u.code.clone()).or_default().push(InviteCodeUseInfo {
322 used_by: u.used_by.into(),
323 used_at: u.used_at.to_rfc3339(),
324 });
325 acc
326 });
327
328 let (codes_by_user, code_info_map): (
329 std::collections::HashMap<uuid::Uuid, Vec<InviteCodeInfo>>,
330 std::collections::HashMap<String, InviteCodeInfo>,
331 ) = all_invite_codes.into_iter().fold(
332 (std::collections::HashMap::new(), std::collections::HashMap::new()),
333 |(mut by_user, mut by_code), ic| {
334 let info = InviteCodeInfo {
335 code: ic.code.clone(),
336 available: ic.available_uses,
337 disabled: ic.disabled.unwrap_or(false),
338 for_account: ic.for_account.into(),
339 created_by: ic.created_by.into(),
340 created_at: ic.created_at.to_rfc3339(),
341 uses: uses_by_code.get(&ic.code).cloned().unwrap_or_default(),
342 };
343 by_code.insert(ic.code.clone(), info.clone());
344 by_user.entry(ic.created_by_user).or_default().push(info);
345 (by_user, by_code)
346 },
347 );
348
349 let infos: Vec<AccountInfo> = users
350 .into_iter()
351 .map(|row| {
352 let invited_by = invited_by_map
353 .get(&row.id)
354 .and_then(|code| code_info_map.get(code).cloned());
355 let invites = codes_by_user.get(&row.id).cloned();
356 AccountInfo {
357 did: row.did.into(),
358 handle: row.handle.into(),
359 email: row.email,
360 indexed_at: row.created_at.to_rfc3339(),
361 invite_note: None,
362 invites_disabled: row.invites_disabled.unwrap_or(false),
363 email_confirmed_at: if row.email_verified {
364 Some(row.created_at.to_rfc3339())
365 } else {
366 None
367 },
368 deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
369 invited_by,
370 invites,
371 }
372 })
373 .collect();
374 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
375}