this repo has no description
1use crate::api::EmptyResponse;
2use crate::api::error::ApiError;
3use crate::auth::BearerAuthAdmin;
4use crate::state::AppState;
5use axum::{
6 Json,
7 extract::{Query, State},
8 http::StatusCode,
9 response::{IntoResponse, Response},
10};
11use serde::{Deserialize, Serialize};
12use tracing::error;
13
14#[derive(Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct DisableInviteCodesInput {
17 pub codes: Option<Vec<String>>,
18 pub accounts: Option<Vec<String>>,
19}
20
21pub async fn disable_invite_codes(
22 State(state): State<AppState>,
23 _auth: BearerAuthAdmin,
24 Json(input): Json<DisableInviteCodesInput>,
25) -> Response {
26 if let Some(codes) = &input.codes {
27 for code in codes {
28 let _ = sqlx::query!(
29 "UPDATE invite_codes SET disabled = TRUE WHERE code = $1",
30 code
31 )
32 .execute(&state.db)
33 .await;
34 }
35 }
36 if let Some(accounts) = &input.accounts {
37 for account in accounts {
38 let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account)
39 .fetch_optional(&state.db)
40 .await;
41 if let Ok(Some(user_row)) = user {
42 let _ = sqlx::query!(
43 "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1",
44 user_row.id
45 )
46 .execute(&state.db)
47 .await;
48 }
49 }
50 }
51 EmptyResponse::ok().into_response()
52}
53
54#[derive(Deserialize)]
55pub struct GetInviteCodesParams {
56 pub sort: Option<String>,
57 pub limit: Option<i64>,
58 pub cursor: Option<String>,
59}
60
61#[derive(Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct InviteCodeInfo {
64 pub code: String,
65 pub available: i32,
66 pub disabled: bool,
67 pub for_account: String,
68 pub created_by: String,
69 pub created_at: String,
70 pub uses: Vec<InviteCodeUseInfo>,
71}
72
73#[derive(Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct InviteCodeUseInfo {
76 pub used_by: String,
77 pub used_at: String,
78}
79
80#[derive(Serialize)]
81pub struct GetInviteCodesOutput {
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub cursor: Option<String>,
84 pub codes: Vec<InviteCodeInfo>,
85}
86
87pub async fn get_invite_codes(
88 State(state): State<AppState>,
89 _auth: BearerAuthAdmin,
90 Query(params): Query<GetInviteCodesParams>,
91) -> Response {
92 let limit = params.limit.unwrap_or(100).clamp(1, 500);
93 let sort = params.sort.as_deref().unwrap_or("recent");
94 let order_clause = match sort {
95 "usage" => "available_uses DESC",
96 _ => "created_at DESC",
97 };
98 let codes_result = if let Some(cursor) = ¶ms.cursor {
99 sqlx::query_as::<
100 _,
101 (
102 String,
103 i32,
104 Option<bool>,
105 uuid::Uuid,
106 chrono::DateTime<chrono::Utc>,
107 ),
108 >(&format!(
109 r#"
110 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
111 FROM invite_codes ic
112 WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1)
113 ORDER BY {}
114 LIMIT $2
115 "#,
116 order_clause
117 ))
118 .bind(cursor)
119 .bind(limit)
120 .fetch_all(&state.db)
121 .await
122 } else {
123 sqlx::query_as::<
124 _,
125 (
126 String,
127 i32,
128 Option<bool>,
129 uuid::Uuid,
130 chrono::DateTime<chrono::Utc>,
131 ),
132 >(&format!(
133 r#"
134 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
135 FROM invite_codes ic
136 ORDER BY {}
137 LIMIT $1
138 "#,
139 order_clause
140 ))
141 .bind(limit)
142 .fetch_all(&state.db)
143 .await
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 ApiError::InternalError(None).into_response();
150 }
151 };
152 let mut codes = Vec::new();
153 for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows {
154 let creator_did =
155 sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user)
156 .fetch_optional(&state.db)
157 .await
158 .ok()
159 .flatten()
160 .unwrap_or_else(|| "unknown".to_string());
161 let uses_result = sqlx::query!(
162 r#"
163 SELECT u.did, icu.used_at
164 FROM invite_code_uses icu
165 JOIN users u ON icu.used_by_user = u.id
166 WHERE icu.code = $1
167 ORDER BY icu.used_at DESC
168 "#,
169 code
170 )
171 .fetch_all(&state.db)
172 .await;
173 let uses = match uses_result {
174 Ok(use_rows) => use_rows
175 .iter()
176 .map(|u| InviteCodeUseInfo {
177 used_by: u.did.clone(),
178 used_at: u.used_at.to_rfc3339(),
179 })
180 .collect(),
181 Err(_) => Vec::new(),
182 };
183 codes.push(InviteCodeInfo {
184 code: code.clone(),
185 available: *available_uses,
186 disabled: disabled.unwrap_or(false),
187 for_account: creator_did.clone(),
188 created_by: creator_did,
189 created_at: created_at.to_rfc3339(),
190 uses,
191 });
192 }
193 let next_cursor = if codes_rows.len() == limit as usize {
194 codes_rows.last().map(|(code, _, _, _, _)| code.clone())
195 } else {
196 None
197 };
198 (
199 StatusCode::OK,
200 Json(GetInviteCodesOutput {
201 cursor: next_cursor,
202 codes,
203 }),
204 )
205 .into_response()
206}
207
208#[derive(Deserialize)]
209pub struct DisableAccountInvitesInput {
210 pub account: String,
211}
212
213pub async fn disable_account_invites(
214 State(state): State<AppState>,
215 _auth: BearerAuthAdmin,
216 Json(input): Json<DisableAccountInvitesInput>,
217) -> Response {
218 let account = input.account.trim();
219 if account.is_empty() {
220 return ApiError::InvalidRequest("account is required".into()).into_response();
221 }
222 let result = sqlx::query!(
223 "UPDATE users SET invites_disabled = TRUE WHERE did = $1",
224 account
225 )
226 .execute(&state.db)
227 .await;
228 match result {
229 Ok(r) => {
230 if r.rows_affected() == 0 {
231 return ApiError::AccountNotFound.into_response();
232 }
233 EmptyResponse::ok().into_response()
234 }
235 Err(e) => {
236 error!("DB error disabling account invites: {:?}", e);
237 ApiError::InternalError(None).into_response()
238 }
239 }
240}
241
242#[derive(Deserialize)]
243pub struct EnableAccountInvitesInput {
244 pub account: String,
245}
246
247pub async fn enable_account_invites(
248 State(state): State<AppState>,
249 _auth: BearerAuthAdmin,
250 Json(input): Json<EnableAccountInvitesInput>,
251) -> Response {
252 let account = input.account.trim();
253 if account.is_empty() {
254 return ApiError::InvalidRequest("account is required".into()).into_response();
255 }
256 let result = sqlx::query!(
257 "UPDATE users SET invites_disabled = FALSE WHERE did = $1",
258 account
259 )
260 .execute(&state.db)
261 .await;
262 match result {
263 Ok(r) => {
264 if r.rows_affected() == 0 {
265 return ApiError::AccountNotFound.into_response();
266 }
267 EmptyResponse::ok().into_response()
268 }
269 Err(e) => {
270 error!("DB error enabling account invites: {:?}", e);
271 ApiError::InternalError(None).into_response()
272 }
273 }
274}