this repo has no description
1use crate::api::ApiError; 2use crate::auth::BearerAuth; 3use crate::state::AppState; 4use crate::util::get_user_id_by_did; 5use axum::{ 6 Json, 7 extract::State, 8 response::{IntoResponse, Response}, 9}; 10use serde::{Deserialize, Serialize}; 11use tracing::error; 12use uuid::Uuid; 13 14#[derive(Deserialize)] 15#[serde(rename_all = "camelCase")] 16pub struct CreateInviteCodeInput { 17 pub use_count: i32, 18 pub for_account: Option<String>, 19} 20 21#[derive(Serialize)] 22pub struct CreateInviteCodeOutput { 23 pub code: String, 24} 25 26pub async fn create_invite_code( 27 State(state): State<AppState>, 28 BearerAuth(auth_user): BearerAuth, 29 Json(input): Json<CreateInviteCodeInput>, 30) -> Response { 31 if input.use_count < 1 { 32 return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 33 } 34 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 35 Ok(id) => id, 36 Err(e) => return ApiError::from(e).into_response(), 37 }; 38 let creator_user_id = if let Some(for_account) = &input.for_account { 39 match sqlx::query!("SELECT id FROM users WHERE did = $1", for_account) 40 .fetch_optional(&state.db) 41 .await 42 { 43 Ok(Some(row)) => row.id, 44 Ok(None) => return ApiError::AccountNotFound.into_response(), 45 Err(e) => { 46 error!("DB error looking up target account: {:?}", e); 47 return ApiError::InternalError.into_response(); 48 } 49 } 50 } else { 51 user_id 52 }; 53 let user_invites_disabled = sqlx::query_scalar!( 54 "SELECT invites_disabled FROM users WHERE did = $1", 55 auth_user.did 56 ) 57 .fetch_optional(&state.db) 58 .await 59 .map_err(|e| { 60 error!("DB error checking invites_disabled: {:?}", e); 61 ApiError::InternalError 62 }) 63 .ok() 64 .flatten() 65 .flatten() 66 .unwrap_or(false); 67 if user_invites_disabled { 68 return ApiError::InvitesDisabled.into_response(); 69 } 70 let code = Uuid::new_v4().to_string(); 71 match sqlx::query!( 72 "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)", 73 code, 74 input.use_count, 75 creator_user_id 76 ) 77 .execute(&state.db) 78 .await 79 { 80 Ok(_) => Json(CreateInviteCodeOutput { code }).into_response(), 81 Err(e) => { 82 error!("DB error creating invite code: {:?}", e); 83 ApiError::InternalError.into_response() 84 } 85 } 86} 87 88#[derive(Deserialize)] 89#[serde(rename_all = "camelCase")] 90pub struct CreateInviteCodesInput { 91 pub code_count: Option<i32>, 92 pub use_count: i32, 93 pub for_accounts: Option<Vec<String>>, 94} 95 96#[derive(Serialize)] 97pub struct CreateInviteCodesOutput { 98 pub codes: Vec<AccountCodes>, 99} 100 101#[derive(Serialize)] 102pub struct AccountCodes { 103 pub account: String, 104 pub codes: Vec<String>, 105} 106 107pub async fn create_invite_codes( 108 State(state): State<AppState>, 109 BearerAuth(auth_user): BearerAuth, 110 Json(input): Json<CreateInviteCodesInput>, 111) -> Response { 112 if input.use_count < 1 { 113 return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 114 } 115 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 116 Ok(id) => id, 117 Err(e) => return ApiError::from(e).into_response(), 118 }; 119 let code_count = input.code_count.unwrap_or(1).max(1); 120 let for_accounts = input.for_accounts.unwrap_or_default(); 121 let mut result_codes = Vec::new(); 122 if for_accounts.is_empty() { 123 let mut codes = Vec::new(); 124 for _ in 0..code_count { 125 let code = Uuid::new_v4().to_string(); 126 if let Err(e) = sqlx::query!( 127 "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)", 128 code, 129 input.use_count, 130 user_id 131 ) 132 .execute(&state.db) 133 .await 134 { 135 error!("DB error creating invite code: {:?}", e); 136 return ApiError::InternalError.into_response(); 137 } 138 codes.push(code); 139 } 140 result_codes.push(AccountCodes { 141 account: "admin".to_string(), 142 codes, 143 }); 144 } else { 145 for account_did in for_accounts { 146 let target_user_id = match sqlx::query!("SELECT id FROM users WHERE did = $1", account_did) 147 .fetch_optional(&state.db) 148 .await 149 { 150 Ok(Some(row)) => row.id, 151 Ok(None) => continue, 152 Err(e) => { 153 error!("DB error looking up target account: {:?}", e); 154 return ApiError::InternalError.into_response(); 155 } 156 }; 157 let mut codes = Vec::new(); 158 for _ in 0..code_count { 159 let code = Uuid::new_v4().to_string(); 160 if let Err(e) = sqlx::query!( 161 "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)", 162 code, 163 input.use_count, 164 target_user_id 165 ) 166 .execute(&state.db) 167 .await 168 { 169 error!("DB error creating invite code: {:?}", e); 170 return ApiError::InternalError.into_response(); 171 } 172 codes.push(code); 173 } 174 result_codes.push(AccountCodes { 175 account: account_did, 176 codes, 177 }); 178 } 179 } 180 Json(CreateInviteCodesOutput { codes: result_codes }).into_response() 181} 182 183#[derive(Deserialize)] 184#[serde(rename_all = "camelCase")] 185pub struct GetAccountInviteCodesParams { 186 pub include_used: Option<bool>, 187 pub create_available: Option<bool>, 188} 189 190#[derive(Serialize)] 191#[serde(rename_all = "camelCase")] 192pub struct InviteCode { 193 pub code: String, 194 pub available: i32, 195 pub disabled: bool, 196 pub for_account: String, 197 pub created_by: String, 198 pub created_at: String, 199 pub uses: Vec<InviteCodeUse>, 200} 201 202#[derive(Serialize)] 203#[serde(rename_all = "camelCase")] 204pub struct InviteCodeUse { 205 pub used_by: String, 206 pub used_at: String, 207} 208 209#[derive(Serialize)] 210pub struct GetAccountInviteCodesOutput { 211 pub codes: Vec<InviteCode>, 212} 213 214pub async fn get_account_invite_codes( 215 State(state): State<AppState>, 216 BearerAuth(auth_user): BearerAuth, 217 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 218) -> Response { 219 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await { 220 Ok(id) => id, 221 Err(e) => return ApiError::from(e).into_response(), 222 }; 223 let include_used = params.include_used.unwrap_or(true); 224 let codes_rows = match sqlx::query!( 225 r#" 226 SELECT code, available_uses, created_at, disabled 227 FROM invite_codes 228 WHERE created_by_user = $1 229 ORDER BY created_at DESC 230 "#, 231 user_id 232 ) 233 .fetch_all(&state.db) 234 .await 235 { 236 Ok(rows) => { 237 if include_used { 238 rows 239 } else { 240 rows.into_iter().filter(|r| r.available_uses > 0).collect() 241 } 242 } 243 Err(e) => { 244 error!("DB error fetching invite codes: {:?}", e); 245 return ApiError::InternalError.into_response(); 246 } 247 }; 248 let mut codes = Vec::new(); 249 for row in codes_rows { 250 let uses = sqlx::query!( 251 r#" 252 SELECT u.did, icu.used_at 253 FROM invite_code_uses icu 254 JOIN users u ON icu.used_by_user = u.id 255 WHERE icu.code = $1 256 ORDER BY icu.used_at DESC 257 "#, 258 row.code 259 ) 260 .fetch_all(&state.db) 261 .await 262 .map(|use_rows| { 263 use_rows 264 .iter() 265 .map(|u| InviteCodeUse { 266 used_by: u.did.clone(), 267 used_at: u.used_at.to_rfc3339(), 268 }) 269 .collect() 270 }) 271 .unwrap_or_default(); 272 codes.push(InviteCode { 273 code: row.code, 274 available: row.available_uses, 275 disabled: row.disabled.unwrap_or(false), 276 for_account: auth_user.did.clone(), 277 created_by: auth_user.did.clone(), 278 created_at: row.created_at.to_rfc3339(), 279 uses, 280 }); 281 } 282 Json(GetAccountInviteCodesOutput { codes }).into_response() 283}