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 sqlx::Row;
11use tracing::error;
12
13#[derive(Deserialize)]
14pub struct GetAccountInfoParams {
15 pub did: String,
16}
17
18#[derive(Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct AccountInfo {
21 pub did: String,
22 pub handle: String,
23 pub email: Option<String>,
24 pub indexed_at: String,
25 pub invite_note: Option<String>,
26 pub invites_disabled: bool,
27 pub email_confirmed_at: Option<String>,
28 pub deactivated_at: Option<String>,
29}
30
31#[derive(Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct GetAccountInfosOutput {
34 pub infos: Vec<AccountInfo>,
35}
36
37pub async fn get_account_info(
38 State(state): State<AppState>,
39 headers: axum::http::HeaderMap,
40 Query(params): Query<GetAccountInfoParams>,
41) -> Response {
42 let auth_header = headers.get("Authorization");
43 if auth_header.is_none() {
44 return (
45 StatusCode::UNAUTHORIZED,
46 Json(json!({"error": "AuthenticationRequired"})),
47 )
48 .into_response();
49 }
50
51 let did = params.did.trim();
52 if did.is_empty() {
53 return (
54 StatusCode::BAD_REQUEST,
55 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
56 )
57 .into_response();
58 }
59
60 let result = sqlx::query(
61 r#"
62 SELECT did, handle, email, created_at
63 FROM users
64 WHERE did = $1
65 "#,
66 )
67 .bind(did)
68 .fetch_optional(&state.db)
69 .await;
70
71 match result {
72 Ok(Some(row)) => {
73 let user_did: String = row.get("did");
74 let handle: String = row.get("handle");
75 let email: String = row.get("email");
76 let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
77
78 (
79 StatusCode::OK,
80 Json(AccountInfo {
81 did: user_did,
82 handle,
83 email: Some(email),
84 indexed_at: created_at.to_rfc3339(),
85 invite_note: None,
86 invites_disabled: false,
87 email_confirmed_at: None,
88 deactivated_at: None,
89 }),
90 )
91 .into_response()
92 }
93 Ok(None) => (
94 StatusCode::NOT_FOUND,
95 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
96 )
97 .into_response(),
98 Err(e) => {
99 error!("DB error in get_account_info: {:?}", e);
100 (
101 StatusCode::INTERNAL_SERVER_ERROR,
102 Json(json!({"error": "InternalError"})),
103 )
104 .into_response()
105 }
106 }
107}
108
109#[derive(Deserialize)]
110pub struct GetAccountInfosParams {
111 pub dids: String,
112}
113
114pub async fn get_account_infos(
115 State(state): State<AppState>,
116 headers: axum::http::HeaderMap,
117 Query(params): Query<GetAccountInfosParams>,
118) -> Response {
119 let auth_header = headers.get("Authorization");
120 if auth_header.is_none() {
121 return (
122 StatusCode::UNAUTHORIZED,
123 Json(json!({"error": "AuthenticationRequired"})),
124 )
125 .into_response();
126 }
127
128 let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect();
129 if dids.is_empty() {
130 return (
131 StatusCode::BAD_REQUEST,
132 Json(json!({"error": "InvalidRequest", "message": "dids is required"})),
133 )
134 .into_response();
135 }
136
137 let mut infos = Vec::new();
138
139 for did in dids {
140 if did.is_empty() {
141 continue;
142 }
143
144 let result = sqlx::query(
145 r#"
146 SELECT did, handle, email, created_at
147 FROM users
148 WHERE did = $1
149 "#,
150 )
151 .bind(did)
152 .fetch_optional(&state.db)
153 .await;
154
155 if let Ok(Some(row)) = result {
156 let user_did: String = row.get("did");
157 let handle: String = row.get("handle");
158 let email: String = row.get("email");
159 let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
160
161 infos.push(AccountInfo {
162 did: user_did,
163 handle,
164 email: Some(email),
165 indexed_at: created_at.to_rfc3339(),
166 invite_note: None,
167 invites_disabled: false,
168 email_confirmed_at: None,
169 deactivated_at: None,
170 });
171 }
172 }
173
174 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
175}
176
177#[derive(Deserialize)]
178pub struct DeleteAccountInput {
179 pub did: String,
180}
181
182pub async fn delete_account(
183 State(state): State<AppState>,
184 headers: axum::http::HeaderMap,
185 Json(input): Json<DeleteAccountInput>,
186) -> Response {
187 let auth_header = headers.get("Authorization");
188 if auth_header.is_none() {
189 return (
190 StatusCode::UNAUTHORIZED,
191 Json(json!({"error": "AuthenticationRequired"})),
192 )
193 .into_response();
194 }
195
196 let did = input.did.trim();
197 if did.is_empty() {
198 return (
199 StatusCode::BAD_REQUEST,
200 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
201 )
202 .into_response();
203 }
204
205 let user = sqlx::query("SELECT id FROM users WHERE did = $1")
206 .bind(did)
207 .fetch_optional(&state.db)
208 .await;
209
210 let user_id: uuid::Uuid = match user {
211 Ok(Some(row)) => row.get("id"),
212 Ok(None) => {
213 return (
214 StatusCode::NOT_FOUND,
215 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
216 )
217 .into_response();
218 }
219 Err(e) => {
220 error!("DB error in delete_account: {:?}", e);
221 return (
222 StatusCode::INTERNAL_SERVER_ERROR,
223 Json(json!({"error": "InternalError"})),
224 )
225 .into_response();
226 }
227 };
228
229 let _ = sqlx::query("DELETE FROM sessions WHERE did = $1")
230 .bind(did)
231 .execute(&state.db)
232 .await;
233
234 let _ = sqlx::query("DELETE FROM records WHERE repo_id = $1")
235 .bind(user_id)
236 .execute(&state.db)
237 .await;
238
239 let _ = sqlx::query("DELETE FROM repos WHERE user_id = $1")
240 .bind(user_id)
241 .execute(&state.db)
242 .await;
243
244 let _ = sqlx::query("DELETE FROM blobs WHERE created_by_user = $1")
245 .bind(user_id)
246 .execute(&state.db)
247 .await;
248
249 let _ = sqlx::query("DELETE FROM user_keys WHERE user_id = $1")
250 .bind(user_id)
251 .execute(&state.db)
252 .await;
253
254 let result = sqlx::query("DELETE FROM users WHERE id = $1")
255 .bind(user_id)
256 .execute(&state.db)
257 .await;
258
259 match result {
260 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
261 Err(e) => {
262 error!("DB error deleting account: {:?}", e);
263 (
264 StatusCode::INTERNAL_SERVER_ERROR,
265 Json(json!({"error": "InternalError"})),
266 )
267 .into_response()
268 }
269 }
270}
271
272#[derive(Deserialize)]
273pub struct UpdateAccountEmailInput {
274 pub account: String,
275 pub email: String,
276}
277
278pub async fn update_account_email(
279 State(state): State<AppState>,
280 headers: axum::http::HeaderMap,
281 Json(input): Json<UpdateAccountEmailInput>,
282) -> Response {
283 let auth_header = headers.get("Authorization");
284 if auth_header.is_none() {
285 return (
286 StatusCode::UNAUTHORIZED,
287 Json(json!({"error": "AuthenticationRequired"})),
288 )
289 .into_response();
290 }
291
292 let account = input.account.trim();
293 let email = input.email.trim();
294
295 if account.is_empty() || email.is_empty() {
296 return (
297 StatusCode::BAD_REQUEST,
298 Json(json!({"error": "InvalidRequest", "message": "account and email are required"})),
299 )
300 .into_response();
301 }
302
303 let result = sqlx::query("UPDATE users SET email = $1 WHERE did = $2")
304 .bind(email)
305 .bind(account)
306 .execute(&state.db)
307 .await;
308
309 match result {
310 Ok(r) => {
311 if r.rows_affected() == 0 {
312 return (
313 StatusCode::NOT_FOUND,
314 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
315 )
316 .into_response();
317 }
318 (StatusCode::OK, Json(json!({}))).into_response()
319 }
320 Err(e) => {
321 error!("DB error updating email: {:?}", e);
322 (
323 StatusCode::INTERNAL_SERVER_ERROR,
324 Json(json!({"error": "InternalError"})),
325 )
326 .into_response()
327 }
328 }
329}
330
331#[derive(Deserialize)]
332pub struct UpdateAccountHandleInput {
333 pub did: String,
334 pub handle: String,
335}
336
337pub async fn update_account_handle(
338 State(state): State<AppState>,
339 headers: axum::http::HeaderMap,
340 Json(input): Json<UpdateAccountHandleInput>,
341) -> Response {
342 let auth_header = headers.get("Authorization");
343 if auth_header.is_none() {
344 return (
345 StatusCode::UNAUTHORIZED,
346 Json(json!({"error": "AuthenticationRequired"})),
347 )
348 .into_response();
349 }
350
351 let did = input.did.trim();
352 let handle = input.handle.trim();
353
354 if did.is_empty() || handle.is_empty() {
355 return (
356 StatusCode::BAD_REQUEST,
357 Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})),
358 )
359 .into_response();
360 }
361
362 if !handle
363 .chars()
364 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
365 {
366 return (
367 StatusCode::BAD_REQUEST,
368 Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})),
369 )
370 .into_response();
371 }
372
373 let existing = sqlx::query("SELECT id FROM users WHERE handle = $1 AND did != $2")
374 .bind(handle)
375 .bind(did)
376 .fetch_optional(&state.db)
377 .await;
378
379 if let Ok(Some(_)) = existing {
380 return (
381 StatusCode::BAD_REQUEST,
382 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})),
383 )
384 .into_response();
385 }
386
387 let result = sqlx::query("UPDATE users SET handle = $1 WHERE did = $2")
388 .bind(handle)
389 .bind(did)
390 .execute(&state.db)
391 .await;
392
393 match result {
394 Ok(r) => {
395 if r.rows_affected() == 0 {
396 return (
397 StatusCode::NOT_FOUND,
398 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
399 )
400 .into_response();
401 }
402 (StatusCode::OK, Json(json!({}))).into_response()
403 }
404 Err(e) => {
405 error!("DB error updating handle: {:?}", e);
406 (
407 StatusCode::INTERNAL_SERVER_ERROR,
408 Json(json!({"error": "InternalError"})),
409 )
410 .into_response()
411 }
412 }
413}
414
415#[derive(Deserialize)]
416pub struct UpdateAccountPasswordInput {
417 pub did: String,
418 pub password: String,
419}
420
421pub async fn update_account_password(
422 State(state): State<AppState>,
423 headers: axum::http::HeaderMap,
424 Json(input): Json<UpdateAccountPasswordInput>,
425) -> Response {
426 let auth_header = headers.get("Authorization");
427 if auth_header.is_none() {
428 return (
429 StatusCode::UNAUTHORIZED,
430 Json(json!({"error": "AuthenticationRequired"})),
431 )
432 .into_response();
433 }
434
435 let did = input.did.trim();
436 let password = input.password.trim();
437
438 if did.is_empty() || password.is_empty() {
439 return (
440 StatusCode::BAD_REQUEST,
441 Json(json!({"error": "InvalidRequest", "message": "did and password are required"})),
442 )
443 .into_response();
444 }
445
446 let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) {
447 Ok(h) => h,
448 Err(e) => {
449 error!("Failed to hash password: {:?}", e);
450 return (
451 StatusCode::INTERNAL_SERVER_ERROR,
452 Json(json!({"error": "InternalError"})),
453 )
454 .into_response();
455 }
456 };
457
458 let result = sqlx::query("UPDATE users SET password_hash = $1 WHERE did = $2")
459 .bind(&password_hash)
460 .bind(did)
461 .execute(&state.db)
462 .await;
463
464 match result {
465 Ok(r) => {
466 if r.rows_affected() == 0 {
467 return (
468 StatusCode::NOT_FOUND,
469 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
470 )
471 .into_response();
472 }
473 (StatusCode::OK, Json(json!({}))).into_response()
474 }
475 Err(e) => {
476 error!("DB error updating password: {:?}", e);
477 (
478 StatusCode::INTERNAL_SERVER_ERROR,
479 Json(json!({"error": "InternalError"})),
480 )
481 .into_response()
482 }
483 }
484}