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