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}