this repo has no description
1use crate::auth::BearerAuthAdmin; 2use crate::state::AppState; 3use axum::{ 4 Json, 5 extract::{Query, State}, 6 http::StatusCode, 7 response::{IntoResponse, Response}, 8}; 9use serde::{Deserialize, Serialize}; 10use serde_json::json; 11use tracing::error; 12 13#[derive(Deserialize)] 14#[serde(rename_all = "camelCase")] 15pub struct DisableInviteCodesInput { 16 pub codes: Option<Vec<String>>, 17 pub accounts: Option<Vec<String>>, 18} 19 20pub async fn disable_invite_codes( 21 State(state): State<AppState>, 22 _auth: BearerAuthAdmin, 23 Json(input): Json<DisableInviteCodesInput>, 24) -> Response { 25 if let Some(codes) = &input.codes { 26 for code in codes { 27 let _ = sqlx::query!( 28 "UPDATE invite_codes SET disabled = TRUE WHERE code = $1", 29 code 30 ) 31 .execute(&state.db) 32 .await; 33 } 34 } 35 if let Some(accounts) = &input.accounts { 36 for account in accounts { 37 let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account) 38 .fetch_optional(&state.db) 39 .await; 40 if let Ok(Some(user_row)) = user { 41 let _ = sqlx::query!( 42 "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1", 43 user_row.id 44 ) 45 .execute(&state.db) 46 .await; 47 } 48 } 49 } 50 (StatusCode::OK, Json(json!({}))).into_response() 51} 52 53#[derive(Deserialize)] 54pub struct GetInviteCodesParams { 55 pub sort: Option<String>, 56 pub limit: Option<i64>, 57 pub cursor: Option<String>, 58} 59 60#[derive(Serialize)] 61#[serde(rename_all = "camelCase")] 62pub struct InviteCodeInfo { 63 pub code: String, 64 pub available: i32, 65 pub disabled: bool, 66 pub for_account: String, 67 pub created_by: String, 68 pub created_at: String, 69 pub uses: Vec<InviteCodeUseInfo>, 70} 71 72#[derive(Serialize)] 73#[serde(rename_all = "camelCase")] 74pub struct InviteCodeUseInfo { 75 pub used_by: String, 76 pub used_at: String, 77} 78 79#[derive(Serialize)] 80pub struct GetInviteCodesOutput { 81 #[serde(skip_serializing_if = "Option::is_none")] 82 pub cursor: Option<String>, 83 pub codes: Vec<InviteCodeInfo>, 84} 85 86pub async fn get_invite_codes( 87 State(state): State<AppState>, 88 _auth: BearerAuthAdmin, 89 Query(params): Query<GetInviteCodesParams>, 90) -> Response { 91 let limit = params.limit.unwrap_or(100).clamp(1, 500); 92 let sort = params.sort.as_deref().unwrap_or("recent"); 93 let order_clause = match sort { 94 "usage" => "available_uses DESC", 95 _ => "created_at DESC", 96 }; 97 let codes_result = if let Some(cursor) = &params.cursor { 98 sqlx::query_as::< 99 _, 100 ( 101 String, 102 i32, 103 Option<bool>, 104 uuid::Uuid, 105 chrono::DateTime<chrono::Utc>, 106 ), 107 >(&format!( 108 r#" 109 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 110 FROM invite_codes ic 111 WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 112 ORDER BY {} 113 LIMIT $2 114 "#, 115 order_clause 116 )) 117 .bind(cursor) 118 .bind(limit) 119 .fetch_all(&state.db) 120 .await 121 } else { 122 sqlx::query_as::< 123 _, 124 ( 125 String, 126 i32, 127 Option<bool>, 128 uuid::Uuid, 129 chrono::DateTime<chrono::Utc>, 130 ), 131 >(&format!( 132 r#" 133 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 134 FROM invite_codes ic 135 ORDER BY {} 136 LIMIT $1 137 "#, 138 order_clause 139 )) 140 .bind(limit) 141 .fetch_all(&state.db) 142 .await 143 }; 144 let codes_rows = match codes_result { 145 Ok(rows) => rows, 146 Err(e) => { 147 error!("DB error fetching invite codes: {:?}", e); 148 return ( 149 StatusCode::INTERNAL_SERVER_ERROR, 150 Json(json!({"error": "InternalError"})), 151 ) 152 .into_response(); 153 } 154 }; 155 let mut codes = Vec::new(); 156 for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows { 157 let creator_did = 158 sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user) 159 .fetch_optional(&state.db) 160 .await 161 .ok() 162 .flatten() 163 .unwrap_or_else(|| "unknown".to_string()); 164 let uses_result = sqlx::query!( 165 r#" 166 SELECT u.did, icu.used_at 167 FROM invite_code_uses icu 168 JOIN users u ON icu.used_by_user = u.id 169 WHERE icu.code = $1 170 ORDER BY icu.used_at DESC 171 "#, 172 code 173 ) 174 .fetch_all(&state.db) 175 .await; 176 let uses = match uses_result { 177 Ok(use_rows) => use_rows 178 .iter() 179 .map(|u| InviteCodeUseInfo { 180 used_by: u.did.clone(), 181 used_at: u.used_at.to_rfc3339(), 182 }) 183 .collect(), 184 Err(_) => Vec::new(), 185 }; 186 codes.push(InviteCodeInfo { 187 code: code.clone(), 188 available: *available_uses, 189 disabled: disabled.unwrap_or(false), 190 for_account: creator_did.clone(), 191 created_by: creator_did, 192 created_at: created_at.to_rfc3339(), 193 uses, 194 }); 195 } 196 let next_cursor = if codes_rows.len() == limit as usize { 197 codes_rows.last().map(|(code, _, _, _, _)| code.clone()) 198 } else { 199 None 200 }; 201 ( 202 StatusCode::OK, 203 Json(GetInviteCodesOutput { 204 cursor: next_cursor, 205 codes, 206 }), 207 ) 208 .into_response() 209} 210 211#[derive(Deserialize)] 212pub struct DisableAccountInvitesInput { 213 pub account: String, 214} 215 216pub async fn disable_account_invites( 217 State(state): State<AppState>, 218 _auth: BearerAuthAdmin, 219 Json(input): Json<DisableAccountInvitesInput>, 220) -> Response { 221 let account = input.account.trim(); 222 if account.is_empty() { 223 return ( 224 StatusCode::BAD_REQUEST, 225 Json(json!({"error": "InvalidRequest", "message": "account is required"})), 226 ) 227 .into_response(); 228 } 229 let result = sqlx::query!( 230 "UPDATE users SET invites_disabled = TRUE WHERE did = $1", 231 account 232 ) 233 .execute(&state.db) 234 .await; 235 match result { 236 Ok(r) => { 237 if r.rows_affected() == 0 { 238 return ( 239 StatusCode::NOT_FOUND, 240 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 241 ) 242 .into_response(); 243 } 244 (StatusCode::OK, Json(json!({}))).into_response() 245 } 246 Err(e) => { 247 error!("DB error disabling account invites: {:?}", e); 248 ( 249 StatusCode::INTERNAL_SERVER_ERROR, 250 Json(json!({"error": "InternalError"})), 251 ) 252 .into_response() 253 } 254 } 255} 256 257#[derive(Deserialize)] 258pub struct EnableAccountInvitesInput { 259 pub account: String, 260} 261 262pub async fn enable_account_invites( 263 State(state): State<AppState>, 264 _auth: BearerAuthAdmin, 265 Json(input): Json<EnableAccountInvitesInput>, 266) -> Response { 267 let account = input.account.trim(); 268 if account.is_empty() { 269 return ( 270 StatusCode::BAD_REQUEST, 271 Json(json!({"error": "InvalidRequest", "message": "account is required"})), 272 ) 273 .into_response(); 274 } 275 let result = sqlx::query!( 276 "UPDATE users SET invites_disabled = FALSE WHERE did = $1", 277 account 278 ) 279 .execute(&state.db) 280 .await; 281 match result { 282 Ok(r) => { 283 if r.rows_affected() == 0 { 284 return ( 285 StatusCode::NOT_FOUND, 286 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 287 ) 288 .into_response(); 289 } 290 (StatusCode::OK, Json(json!({}))).into_response() 291 } 292 Err(e) => { 293 error!("DB error enabling account invites: {:?}", e); 294 ( 295 StatusCode::INTERNAL_SERVER_ERROR, 296 Json(json!({"error": "InternalError"})), 297 ) 298 .into_response() 299 } 300 } 301}