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