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(
129 db: &sqlx::PgPool,
130 user_id: uuid::Uuid,
131) -> Option<InviteCodeInfo> {
132 let use_row = sqlx::query!(
133 r#"
134 SELECT icu.code
135 FROM invite_code_uses icu
136 WHERE icu.used_by_user = $1
137 LIMIT 1
138 "#,
139 user_id
140 )
141 .fetch_optional(db)
142 .await
143 .ok()??;
144 get_invite_code_info(db, &use_row.code).await
145}
146
147async fn get_invites_for_user(
148 db: &sqlx::PgPool,
149 user_id: uuid::Uuid,
150) -> Option<Vec<InviteCodeInfo>> {
151 let codes = sqlx::query_scalar!(
152 r#"
153 SELECT code FROM invite_codes WHERE created_by_user = $1
154 "#,
155 user_id
156 )
157 .fetch_all(db)
158 .await
159 .ok()?;
160 if codes.is_empty() {
161 return None;
162 }
163 let mut invites = Vec::new();
164 for code in codes {
165 if let Some(info) = get_invite_code_info(db, &code).await {
166 invites.push(info);
167 }
168 }
169 if invites.is_empty() {
170 None
171 } else {
172 Some(invites)
173 }
174}
175
176async fn get_invite_code_info(db: &sqlx::PgPool, code: &str) -> Option<InviteCodeInfo> {
177 let row = sqlx::query!(
178 r#"
179 SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by
180 FROM invite_codes ic
181 JOIN users u ON ic.created_by_user = u.id
182 WHERE ic.code = $1
183 "#,
184 code
185 )
186 .fetch_optional(db)
187 .await
188 .ok()??;
189 let uses = sqlx::query!(
190 r#"
191 SELECT u.did as used_by, icu.used_at
192 FROM invite_code_uses icu
193 JOIN users u ON icu.used_by_user = u.id
194 WHERE icu.code = $1
195 "#,
196 code
197 )
198 .fetch_all(db)
199 .await
200 .ok()?;
201 Some(InviteCodeInfo {
202 code: row.code,
203 available: row.available_uses,
204 disabled: row.disabled.unwrap_or(false),
205 for_account: row.for_account,
206 created_by: row.created_by,
207 created_at: row.created_at.to_rfc3339(),
208 uses: uses
209 .into_iter()
210 .map(|u| InviteCodeUseInfo {
211 used_by: u.used_by,
212 used_at: u.used_at.to_rfc3339(),
213 })
214 .collect(),
215 })
216}
217
218pub async fn get_account_infos(
219 State(state): State<AppState>,
220 _auth: BearerAuthAdmin,
221 RawQuery(raw_query): RawQuery,
222) -> Response {
223 let dids = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids");
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 mut infos = Vec::new();
232 for did in &dids {
233 if did.is_empty() {
234 continue;
235 }
236 let result = sqlx::query!(
237 r#"
238 SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
239 FROM users
240 WHERE did = $1
241 "#,
242 did
243 )
244 .fetch_optional(&state.db)
245 .await;
246 if let Ok(Some(row)) = result {
247 let invited_by = get_invited_by(&state.db, row.id).await;
248 let invites = get_invites_for_user(&state.db, row.id).await;
249 infos.push(AccountInfo {
250 did: row.did,
251 handle: row.handle,
252 email: row.email,
253 indexed_at: row.created_at.to_rfc3339(),
254 invite_note: None,
255 invites_disabled: row.invites_disabled.unwrap_or(false),
256 email_confirmed_at: if row.email_verified {
257 Some(row.created_at.to_rfc3339())
258 } else {
259 None
260 },
261 deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
262 invited_by,
263 invites,
264 });
265 }
266 }
267 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
268}