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