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}