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