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