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 #[serde(skip_serializing_if = "Option::is_none")]
82 pub cursor: Option<String>,
83 pub codes: Vec<InviteCodeInfo>,
84}
85
86pub async fn get_invite_codes(
87 State(state): State<AppState>,
88 _auth: BearerAuthAdmin,
89 Query(params): Query<GetInviteCodesParams>,
90) -> Response {
91 let limit = params.limit.unwrap_or(100).clamp(1, 500);
92 let sort = params.sort.as_deref().unwrap_or("recent");
93 let order_clause = match sort {
94 "usage" => "available_uses DESC",
95 _ => "created_at DESC",
96 };
97 let codes_result = if let Some(cursor) = ¶ms.cursor {
98 sqlx::query_as::<
99 _,
100 (
101 String,
102 i32,
103 Option<bool>,
104 uuid::Uuid,
105 chrono::DateTime<chrono::Utc>,
106 ),
107 >(&format!(
108 r#"
109 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
110 FROM invite_codes ic
111 WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1)
112 ORDER BY {}
113 LIMIT $2
114 "#,
115 order_clause
116 ))
117 .bind(cursor)
118 .bind(limit)
119 .fetch_all(&state.db)
120 .await
121 } else {
122 sqlx::query_as::<
123 _,
124 (
125 String,
126 i32,
127 Option<bool>,
128 uuid::Uuid,
129 chrono::DateTime<chrono::Utc>,
130 ),
131 >(&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 let codes_rows = match codes_result {
145 Ok(rows) => rows,
146 Err(e) => {
147 error!("DB error fetching invite codes: {:?}", e);
148 return (
149 StatusCode::INTERNAL_SERVER_ERROR,
150 Json(json!({"error": "InternalError"})),
151 )
152 .into_response();
153 }
154 };
155 let mut codes = Vec::new();
156 for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows {
157 let creator_did =
158 sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user)
159 .fetch_optional(&state.db)
160 .await
161 .ok()
162 .flatten()
163 .unwrap_or_else(|| "unknown".to_string());
164 let uses_result = sqlx::query!(
165 r#"
166 SELECT u.did, icu.used_at
167 FROM invite_code_uses icu
168 JOIN users u ON icu.used_by_user = u.id
169 WHERE icu.code = $1
170 ORDER BY icu.used_at DESC
171 "#,
172 code
173 )
174 .fetch_all(&state.db)
175 .await;
176 let uses = match uses_result {
177 Ok(use_rows) => use_rows
178 .iter()
179 .map(|u| InviteCodeUseInfo {
180 used_by: u.did.clone(),
181 used_at: u.used_at.to_rfc3339(),
182 })
183 .collect(),
184 Err(_) => Vec::new(),
185 };
186 codes.push(InviteCodeInfo {
187 code: code.clone(),
188 available: *available_uses,
189 disabled: disabled.unwrap_or(false),
190 for_account: creator_did.clone(),
191 created_by: creator_did,
192 created_at: created_at.to_rfc3339(),
193 uses,
194 });
195 }
196 let next_cursor = if codes_rows.len() == limit as usize {
197 codes_rows.last().map(|(code, _, _, _, _)| code.clone())
198 } else {
199 None
200 };
201 (
202 StatusCode::OK,
203 Json(GetInviteCodesOutput {
204 cursor: next_cursor,
205 codes,
206 }),
207 )
208 .into_response()
209}
210
211#[derive(Deserialize)]
212pub struct DisableAccountInvitesInput {
213 pub account: String,
214}
215
216pub async fn disable_account_invites(
217 State(state): State<AppState>,
218 _auth: BearerAuthAdmin,
219 Json(input): Json<DisableAccountInvitesInput>,
220) -> Response {
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!(
230 "UPDATE users SET invites_disabled = TRUE WHERE did = $1",
231 account
232 )
233 .execute(&state.db)
234 .await;
235 match result {
236 Ok(r) => {
237 if r.rows_affected() == 0 {
238 return (
239 StatusCode::NOT_FOUND,
240 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
241 )
242 .into_response();
243 }
244 (StatusCode::OK, Json(json!({}))).into_response()
245 }
246 Err(e) => {
247 error!("DB error disabling account invites: {:?}", e);
248 (
249 StatusCode::INTERNAL_SERVER_ERROR,
250 Json(json!({"error": "InternalError"})),
251 )
252 .into_response()
253 }
254 }
255}
256
257#[derive(Deserialize)]
258pub struct EnableAccountInvitesInput {
259 pub account: String,
260}
261
262pub async fn enable_account_invites(
263 State(state): State<AppState>,
264 _auth: BearerAuthAdmin,
265 Json(input): Json<EnableAccountInvitesInput>,
266) -> Response {
267 let account = input.account.trim();
268 if account.is_empty() {
269 return (
270 StatusCode::BAD_REQUEST,
271 Json(json!({"error": "InvalidRequest", "message": "account is required"})),
272 )
273 .into_response();
274 }
275 let result = sqlx::query!(
276 "UPDATE users SET invites_disabled = FALSE WHERE did = $1",
277 account
278 )
279 .execute(&state.db)
280 .await;
281 match result {
282 Ok(r) => {
283 if r.rows_affected() == 0 {
284 return (
285 StatusCode::NOT_FOUND,
286 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
287 )
288 .into_response();
289 }
290 (StatusCode::OK, Json(json!({}))).into_response()
291 }
292 Err(e) => {
293 error!("DB error enabling account invites: {:?}", e);
294 (
295 StatusCode::INTERNAL_SERVER_ERROR,
296 Json(json!({"error": "InternalError"})),
297 )
298 .into_response()
299 }
300 }
301}