this repo has no description
1use crate::api::ApiError;
2use crate::auth::BearerAuth;
3use crate::state::AppState;
4use crate::util::get_user_id_by_did;
5use axum::{
6 Json,
7 extract::State,
8 response::{IntoResponse, Response},
9};
10use serde::{Deserialize, Serialize};
11use tracing::error;
12use uuid::Uuid;
13
14#[derive(Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct CreateInviteCodeInput {
17 pub use_count: i32,
18 pub for_account: Option<String>,
19}
20
21#[derive(Serialize)]
22pub struct CreateInviteCodeOutput {
23 pub code: String,
24}
25
26pub async fn create_invite_code(
27 State(state): State<AppState>,
28 BearerAuth(auth_user): BearerAuth,
29 Json(input): Json<CreateInviteCodeInput>,
30) -> Response {
31 if input.use_count < 1 {
32 return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
33 }
34 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
35 Ok(id) => id,
36 Err(e) => return ApiError::from(e).into_response(),
37 };
38 let creator_user_id = if let Some(for_account) = &input.for_account {
39 match sqlx::query!("SELECT id FROM users WHERE did = $1", for_account)
40 .fetch_optional(&state.db)
41 .await
42 {
43 Ok(Some(row)) => row.id,
44 Ok(None) => return ApiError::AccountNotFound.into_response(),
45 Err(e) => {
46 error!("DB error looking up target account: {:?}", e);
47 return ApiError::InternalError.into_response();
48 }
49 }
50 } else {
51 user_id
52 };
53 let user_invites_disabled = sqlx::query_scalar!(
54 "SELECT invites_disabled FROM users WHERE did = $1",
55 auth_user.did
56 )
57 .fetch_optional(&state.db)
58 .await
59 .map_err(|e| {
60 error!("DB error checking invites_disabled: {:?}", e);
61 ApiError::InternalError
62 })
63 .ok()
64 .flatten()
65 .flatten()
66 .unwrap_or(false);
67 if user_invites_disabled {
68 return ApiError::InvitesDisabled.into_response();
69 }
70 let code = Uuid::new_v4().to_string();
71 match sqlx::query!(
72 "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
73 code,
74 input.use_count,
75 creator_user_id
76 )
77 .execute(&state.db)
78 .await
79 {
80 Ok(_) => Json(CreateInviteCodeOutput { code }).into_response(),
81 Err(e) => {
82 error!("DB error creating invite code: {:?}", e);
83 ApiError::InternalError.into_response()
84 }
85 }
86}
87
88#[derive(Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct CreateInviteCodesInput {
91 pub code_count: Option<i32>,
92 pub use_count: i32,
93 pub for_accounts: Option<Vec<String>>,
94}
95
96#[derive(Serialize)]
97pub struct CreateInviteCodesOutput {
98 pub codes: Vec<AccountCodes>,
99}
100
101#[derive(Serialize)]
102pub struct AccountCodes {
103 pub account: String,
104 pub codes: Vec<String>,
105}
106
107pub async fn create_invite_codes(
108 State(state): State<AppState>,
109 BearerAuth(auth_user): BearerAuth,
110 Json(input): Json<CreateInviteCodesInput>,
111) -> Response {
112 if input.use_count < 1 {
113 return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
114 }
115 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
116 Ok(id) => id,
117 Err(e) => return ApiError::from(e).into_response(),
118 };
119 let code_count = input.code_count.unwrap_or(1).max(1);
120 let for_accounts = input.for_accounts.unwrap_or_default();
121 let mut result_codes = Vec::new();
122 if for_accounts.is_empty() {
123 let mut codes = Vec::new();
124 for _ in 0..code_count {
125 let code = Uuid::new_v4().to_string();
126 if let Err(e) = sqlx::query!(
127 "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
128 code,
129 input.use_count,
130 user_id
131 )
132 .execute(&state.db)
133 .await
134 {
135 error!("DB error creating invite code: {:?}", e);
136 return ApiError::InternalError.into_response();
137 }
138 codes.push(code);
139 }
140 result_codes.push(AccountCodes {
141 account: "admin".to_string(),
142 codes,
143 });
144 } else {
145 for account_did in for_accounts {
146 let target_user_id = match sqlx::query!("SELECT id FROM users WHERE did = $1", account_did)
147 .fetch_optional(&state.db)
148 .await
149 {
150 Ok(Some(row)) => row.id,
151 Ok(None) => continue,
152 Err(e) => {
153 error!("DB error looking up target account: {:?}", e);
154 return ApiError::InternalError.into_response();
155 }
156 };
157 let mut codes = Vec::new();
158 for _ in 0..code_count {
159 let code = Uuid::new_v4().to_string();
160 if let Err(e) = sqlx::query!(
161 "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
162 code,
163 input.use_count,
164 target_user_id
165 )
166 .execute(&state.db)
167 .await
168 {
169 error!("DB error creating invite code: {:?}", e);
170 return ApiError::InternalError.into_response();
171 }
172 codes.push(code);
173 }
174 result_codes.push(AccountCodes {
175 account: account_did,
176 codes,
177 });
178 }
179 }
180 Json(CreateInviteCodesOutput { codes: result_codes }).into_response()
181}
182
183#[derive(Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct GetAccountInviteCodesParams {
186 pub include_used: Option<bool>,
187 pub create_available: Option<bool>,
188}
189
190#[derive(Serialize)]
191#[serde(rename_all = "camelCase")]
192pub struct InviteCode {
193 pub code: String,
194 pub available: i32,
195 pub disabled: bool,
196 pub for_account: String,
197 pub created_by: String,
198 pub created_at: String,
199 pub uses: Vec<InviteCodeUse>,
200}
201
202#[derive(Serialize)]
203#[serde(rename_all = "camelCase")]
204pub struct InviteCodeUse {
205 pub used_by: String,
206 pub used_at: String,
207}
208
209#[derive(Serialize)]
210pub struct GetAccountInviteCodesOutput {
211 pub codes: Vec<InviteCode>,
212}
213
214pub async fn get_account_invite_codes(
215 State(state): State<AppState>,
216 BearerAuth(auth_user): BearerAuth,
217 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
218) -> Response {
219 let user_id = match get_user_id_by_did(&state.db, &auth_user.did).await {
220 Ok(id) => id,
221 Err(e) => return ApiError::from(e).into_response(),
222 };
223 let include_used = params.include_used.unwrap_or(true);
224 let codes_rows = match sqlx::query!(
225 r#"
226 SELECT code, available_uses, created_at, disabled
227 FROM invite_codes
228 WHERE created_by_user = $1
229 ORDER BY created_at DESC
230 "#,
231 user_id
232 )
233 .fetch_all(&state.db)
234 .await
235 {
236 Ok(rows) => {
237 if include_used {
238 rows
239 } else {
240 rows.into_iter().filter(|r| r.available_uses > 0).collect()
241 }
242 }
243 Err(e) => {
244 error!("DB error fetching invite codes: {:?}", e);
245 return ApiError::InternalError.into_response();
246 }
247 };
248 let mut codes = Vec::new();
249 for row in codes_rows {
250 let uses = sqlx::query!(
251 r#"
252 SELECT u.did, icu.used_at
253 FROM invite_code_uses icu
254 JOIN users u ON icu.used_by_user = u.id
255 WHERE icu.code = $1
256 ORDER BY icu.used_at DESC
257 "#,
258 row.code
259 )
260 .fetch_all(&state.db)
261 .await
262 .map(|use_rows| {
263 use_rows
264 .iter()
265 .map(|u| InviteCodeUse {
266 used_by: u.did.clone(),
267 used_at: u.used_at.to_rfc3339(),
268 })
269 .collect()
270 })
271 .unwrap_or_default();
272 codes.push(InviteCode {
273 code: row.code,
274 available: row.available_uses,
275 disabled: row.disabled.unwrap_or(false),
276 for_account: auth_user.did.clone(),
277 created_by: auth_user.did.clone(),
278 created_at: row.created_at.to_rfc3339(),
279 uses,
280 });
281 }
282 Json(GetAccountInviteCodesOutput { codes }).into_response()
283}