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