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 = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids");
221 if dids.is_empty() {
222 return (
223 StatusCode::BAD_REQUEST,
224 Json(json!({"error": "InvalidRequest", "message": "dids is required"})),
225 )
226 .into_response();
227 }
228 let mut infos = Vec::new();
229 for did in &dids {
230 if did.is_empty() {
231 continue;
232 }
233 let result = sqlx::query!(
234 r#"
235 SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
236 FROM users
237 WHERE did = $1
238 "#,
239 did
240 )
241 .fetch_optional(&state.db)
242 .await;
243 if let Ok(Some(row)) = result {
244 let invited_by = get_invited_by(&state.db, row.id).await;
245 let invites = get_invites_for_user(&state.db, row.id).await;
246 infos.push(AccountInfo {
247 did: row.did,
248 handle: row.handle,
249 email: row.email,
250 indexed_at: row.created_at.to_rfc3339(),
251 invite_note: None,
252 invites_disabled: row.invites_disabled.unwrap_or(false),
253 email_confirmed_at: if row.email_verified {
254 Some(row.created_at.to_rfc3339())
255 } else {
256 None
257 },
258 deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
259 invited_by,
260 invites,
261 });
262 }
263 }
264 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
265}