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