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}