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