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 pub cursor: Option<String>, 82 pub codes: Vec<InviteCodeInfo>, 83} 84 85pub async fn get_invite_codes( 86 State(state): State<AppState>, 87 _auth: BearerAuthAdmin, 88 Query(params): Query<GetInviteCodesParams>, 89) -> Response { 90 let limit = params.limit.unwrap_or(100).clamp(1, 500); 91 let sort = params.sort.as_deref().unwrap_or("recent"); 92 let order_clause = match sort { 93 "usage" => "available_uses DESC", 94 _ => "created_at DESC", 95 }; 96 let codes_result = if let Some(cursor) = &params.cursor { 97 sqlx::query_as::< 98 _, 99 ( 100 String, 101 i32, 102 Option<bool>, 103 uuid::Uuid, 104 chrono::DateTime<chrono::Utc>, 105 ), 106 >(&format!( 107 r#" 108 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 109 FROM invite_codes ic 110 WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 111 ORDER BY {} 112 LIMIT $2 113 "#, 114 order_clause 115 )) 116 .bind(cursor) 117 .bind(limit) 118 .fetch_all(&state.db) 119 .await 120 } else { 121 sqlx::query_as::< 122 _, 123 ( 124 String, 125 i32, 126 Option<bool>, 127 uuid::Uuid, 128 chrono::DateTime<chrono::Utc>, 129 ), 130 >(&format!( 131 r#" 132 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 133 FROM invite_codes ic 134 ORDER BY {} 135 LIMIT $1 136 "#, 137 order_clause 138 )) 139 .bind(limit) 140 .fetch_all(&state.db) 141 .await 142 }; 143 let codes_rows = match codes_result { 144 Ok(rows) => rows, 145 Err(e) => { 146 error!("DB error fetching invite codes: {:?}", e); 147 return ( 148 StatusCode::INTERNAL_SERVER_ERROR, 149 Json(json!({"error": "InternalError"})), 150 ) 151 .into_response(); 152 } 153 }; 154 let mut codes = Vec::new(); 155 for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows { 156 let creator_did = 157 sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user) 158 .fetch_optional(&state.db) 159 .await 160 .ok() 161 .flatten() 162 .unwrap_or_else(|| "unknown".to_string()); 163 let uses_result = sqlx::query!( 164 r#" 165 SELECT u.did, icu.used_at 166 FROM invite_code_uses icu 167 JOIN users u ON icu.used_by_user = u.id 168 WHERE icu.code = $1 169 ORDER BY icu.used_at DESC 170 "#, 171 code 172 ) 173 .fetch_all(&state.db) 174 .await; 175 let uses = match uses_result { 176 Ok(use_rows) => use_rows 177 .iter() 178 .map(|u| InviteCodeUseInfo { 179 used_by: u.did.clone(), 180 used_at: u.used_at.to_rfc3339(), 181 }) 182 .collect(), 183 Err(_) => Vec::new(), 184 }; 185 codes.push(InviteCodeInfo { 186 code: code.clone(), 187 available: *available_uses, 188 disabled: disabled.unwrap_or(false), 189 for_account: creator_did.clone(), 190 created_by: creator_did, 191 created_at: created_at.to_rfc3339(), 192 uses, 193 }); 194 } 195 let next_cursor = if codes_rows.len() == limit as usize { 196 codes_rows.last().map(|(code, _, _, _, _)| code.clone()) 197 } else { 198 None 199 }; 200 ( 201 StatusCode::OK, 202 Json(GetInviteCodesOutput { 203 cursor: next_cursor, 204 codes, 205 }), 206 ) 207 .into_response() 208} 209 210#[derive(Deserialize)] 211pub struct DisableAccountInvitesInput { 212 pub account: String, 213} 214 215pub async fn disable_account_invites( 216 State(state): State<AppState>, 217 _auth: BearerAuthAdmin, 218 Json(input): Json<DisableAccountInvitesInput>, 219) -> Response { 220 let account = input.account.trim(); 221 if account.is_empty() { 222 return ( 223 StatusCode::BAD_REQUEST, 224 Json(json!({"error": "InvalidRequest", "message": "account is required"})), 225 ) 226 .into_response(); 227 } 228 let result = sqlx::query!( 229 "UPDATE users SET invites_disabled = TRUE WHERE did = $1", 230 account 231 ) 232 .execute(&state.db) 233 .await; 234 match result { 235 Ok(r) => { 236 if r.rows_affected() == 0 { 237 return ( 238 StatusCode::NOT_FOUND, 239 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 240 ) 241 .into_response(); 242 } 243 (StatusCode::OK, Json(json!({}))).into_response() 244 } 245 Err(e) => { 246 error!("DB error disabling account invites: {:?}", e); 247 ( 248 StatusCode::INTERNAL_SERVER_ERROR, 249 Json(json!({"error": "InternalError"})), 250 ) 251 .into_response() 252 } 253 } 254} 255 256#[derive(Deserialize)] 257pub struct EnableAccountInvitesInput { 258 pub account: String, 259} 260 261pub async fn enable_account_invites( 262 State(state): State<AppState>, 263 _auth: BearerAuthAdmin, 264 Json(input): Json<EnableAccountInvitesInput>, 265) -> Response { 266 let account = input.account.trim(); 267 if account.is_empty() { 268 return ( 269 StatusCode::BAD_REQUEST, 270 Json(json!({"error": "InvalidRequest", "message": "account is required"})), 271 ) 272 .into_response(); 273 } 274 let result = sqlx::query!( 275 "UPDATE users SET invites_disabled = FALSE WHERE did = $1", 276 account 277 ) 278 .execute(&state.db) 279 .await; 280 match result { 281 Ok(r) => { 282 if r.rows_affected() == 0 { 283 return ( 284 StatusCode::NOT_FOUND, 285 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 286 ) 287 .into_response(); 288 } 289 (StatusCode::OK, Json(json!({}))).into_response() 290 } 291 Err(e) => { 292 error!("DB error enabling account invites: {:?}", e); 293 ( 294 StatusCode::INTERNAL_SERVER_ERROR, 295 Json(json!({"error": "InternalError"})), 296 ) 297 .into_response() 298 } 299 } 300}