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