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