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) = ¶ms.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}