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
12#[derive(Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct DisableInviteCodesInput {
15 pub codes: Option<Vec<String>>,
16 pub accounts: Option<Vec<String>>,
17}
18
19pub async fn disable_invite_codes(
20 State(state): State<AppState>,
21 headers: axum::http::HeaderMap,
22 Json(input): Json<DisableInviteCodesInput>,
23) -> Response {
24 let auth_header = headers.get("Authorization");
25 if auth_header.is_none() {
26 return (
27 StatusCode::UNAUTHORIZED,
28 Json(json!({"error": "AuthenticationRequired"})),
29 )
30 .into_response();
31 }
32
33 if let Some(codes) = &input.codes {
34 for code in codes {
35 let _ = sqlx::query!("UPDATE invite_codes SET disabled = TRUE WHERE code = $1", code)
36 .execute(&state.db)
37 .await;
38 }
39 }
40
41 if let Some(accounts) = &input.accounts {
42 for account in accounts {
43 let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account)
44 .fetch_optional(&state.db)
45 .await;
46
47 if let Ok(Some(user_row)) = user {
48 let _ = sqlx::query!(
49 "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1",
50 user_row.id
51 )
52 .execute(&state.db)
53 .await;
54 }
55 }
56 }
57
58 (StatusCode::OK, Json(json!({}))).into_response()
59}
60
61#[derive(Deserialize)]
62pub struct GetSubjectStatusParams {
63 pub did: Option<String>,
64 pub uri: Option<String>,
65 pub blob: Option<String>,
66}
67
68#[derive(Serialize)]
69pub struct SubjectStatus {
70 pub subject: serde_json::Value,
71 pub takedown: Option<StatusAttr>,
72 pub deactivated: Option<StatusAttr>,
73}
74
75#[derive(Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct StatusAttr {
78 pub applied: bool,
79 pub r#ref: Option<String>,
80}
81
82pub async fn get_subject_status(
83 State(state): State<AppState>,
84 headers: axum::http::HeaderMap,
85 Query(params): Query<GetSubjectStatusParams>,
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
96 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() {
97 return (
98 StatusCode::BAD_REQUEST,
99 Json(json!({"error": "InvalidRequest", "message": "Must provide did, uri, or blob"})),
100 )
101 .into_response();
102 }
103
104 if let Some(did) = ¶ms.did {
105 let user = sqlx::query!(
106 "SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1",
107 did
108 )
109 .fetch_optional(&state.db)
110 .await;
111
112 match user {
113 Ok(Some(row)) => {
114 let deactivated = row.deactivated_at.map(|_| StatusAttr {
115 applied: true,
116 r#ref: None,
117 });
118 let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
119 applied: true,
120 r#ref: Some(r.clone()),
121 });
122
123 return (
124 StatusCode::OK,
125 Json(SubjectStatus {
126 subject: json!({
127 "$type": "com.atproto.admin.defs#repoRef",
128 "did": row.did
129 }),
130 takedown,
131 deactivated,
132 }),
133 )
134 .into_response();
135 }
136 Ok(None) => {
137 return (
138 StatusCode::NOT_FOUND,
139 Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
140 )
141 .into_response();
142 }
143 Err(e) => {
144 error!("DB error in get_subject_status: {:?}", e);
145 return (
146 StatusCode::INTERNAL_SERVER_ERROR,
147 Json(json!({"error": "InternalError"})),
148 )
149 .into_response();
150 }
151 }
152 }
153
154 if let Some(uri) = ¶ms.uri {
155 let record = sqlx::query!(
156 "SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1",
157 uri
158 )
159 .fetch_optional(&state.db)
160 .await;
161
162 match record {
163 Ok(Some(row)) => {
164 let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
165 applied: true,
166 r#ref: Some(r.clone()),
167 });
168
169 return (
170 StatusCode::OK,
171 Json(SubjectStatus {
172 subject: json!({
173 "$type": "com.atproto.repo.strongRef",
174 "uri": uri,
175 "cid": uri
176 }),
177 takedown,
178 deactivated: None,
179 }),
180 )
181 .into_response();
182 }
183 Ok(None) => {
184 return (
185 StatusCode::NOT_FOUND,
186 Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
187 )
188 .into_response();
189 }
190 Err(e) => {
191 error!("DB error in get_subject_status: {:?}", e);
192 return (
193 StatusCode::INTERNAL_SERVER_ERROR,
194 Json(json!({"error": "InternalError"})),
195 )
196 .into_response();
197 }
198 }
199 }
200
201 if let Some(blob_cid) = ¶ms.blob {
202 let blob = sqlx::query!("SELECT cid, takedown_ref FROM blobs WHERE cid = $1", blob_cid)
203 .fetch_optional(&state.db)
204 .await;
205
206 match blob {
207 Ok(Some(row)) => {
208 let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
209 applied: true,
210 r#ref: Some(r.clone()),
211 });
212
213 return (
214 StatusCode::OK,
215 Json(SubjectStatus {
216 subject: json!({
217 "$type": "com.atproto.admin.defs#repoBlobRef",
218 "did": "",
219 "cid": row.cid
220 }),
221 takedown,
222 deactivated: None,
223 }),
224 )
225 .into_response();
226 }
227 Ok(None) => {
228 return (
229 StatusCode::NOT_FOUND,
230 Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
231 )
232 .into_response();
233 }
234 Err(e) => {
235 error!("DB error in get_subject_status: {:?}", e);
236 return (
237 StatusCode::INTERNAL_SERVER_ERROR,
238 Json(json!({"error": "InternalError"})),
239 )
240 .into_response();
241 }
242 }
243 }
244
245 (
246 StatusCode::BAD_REQUEST,
247 Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),
248 )
249 .into_response()
250}
251
252#[derive(Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct UpdateSubjectStatusInput {
255 pub subject: serde_json::Value,
256 pub takedown: Option<StatusAttrInput>,
257 pub deactivated: Option<StatusAttrInput>,
258}
259
260#[derive(Deserialize)]
261pub struct StatusAttrInput {
262 pub apply: bool,
263 pub r#ref: Option<String>,
264}
265
266pub async fn update_subject_status(
267 State(state): State<AppState>,
268 headers: axum::http::HeaderMap,
269 Json(input): Json<UpdateSubjectStatusInput>,
270) -> Response {
271 let auth_header = headers.get("Authorization");
272 if auth_header.is_none() {
273 return (
274 StatusCode::UNAUTHORIZED,
275 Json(json!({"error": "AuthenticationRequired"})),
276 )
277 .into_response();
278 }
279
280 let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
281
282 match subject_type {
283 Some("com.atproto.admin.defs#repoRef") => {
284 let did = input.subject.get("did").and_then(|d| d.as_str());
285 if let Some(did) = did {
286 if let Some(takedown) = &input.takedown {
287 let takedown_ref = if takedown.apply {
288 takedown.r#ref.clone()
289 } else {
290 None
291 };
292 let _ = sqlx::query!(
293 "UPDATE users SET takedown_ref = $1 WHERE did = $2",
294 takedown_ref,
295 did
296 )
297 .execute(&state.db)
298 .await;
299 }
300
301 if let Some(deactivated) = &input.deactivated {
302 if deactivated.apply {
303 let _ = sqlx::query!(
304 "UPDATE users SET deactivated_at = NOW() WHERE did = $1",
305 did
306 )
307 .execute(&state.db)
308 .await;
309 } else {
310 let _ = sqlx::query!(
311 "UPDATE users SET deactivated_at = NULL WHERE did = $1",
312 did
313 )
314 .execute(&state.db)
315 .await;
316 }
317 }
318
319 return (
320 StatusCode::OK,
321 Json(json!({
322 "subject": input.subject,
323 "takedown": input.takedown.as_ref().map(|t| json!({
324 "applied": t.apply,
325 "ref": t.r#ref
326 })),
327 "deactivated": input.deactivated.as_ref().map(|d| json!({
328 "applied": d.apply
329 }))
330 })),
331 )
332 .into_response();
333 }
334 }
335 Some("com.atproto.repo.strongRef") => {
336 let uri = input.subject.get("uri").and_then(|u| u.as_str());
337 if let Some(uri) = uri {
338 if let Some(takedown) = &input.takedown {
339 let takedown_ref = if takedown.apply {
340 takedown.r#ref.clone()
341 } else {
342 None
343 };
344 let _ = sqlx::query!(
345 "UPDATE records SET takedown_ref = $1 WHERE record_cid = $2",
346 takedown_ref,
347 uri
348 )
349 .execute(&state.db)
350 .await;
351 }
352
353 return (
354 StatusCode::OK,
355 Json(json!({
356 "subject": input.subject,
357 "takedown": input.takedown.as_ref().map(|t| json!({
358 "applied": t.apply,
359 "ref": t.r#ref
360 }))
361 })),
362 )
363 .into_response();
364 }
365 }
366 Some("com.atproto.admin.defs#repoBlobRef") => {
367 let cid = input.subject.get("cid").and_then(|c| c.as_str());
368 if let Some(cid) = cid {
369 if let Some(takedown) = &input.takedown {
370 let takedown_ref = if takedown.apply {
371 takedown.r#ref.clone()
372 } else {
373 None
374 };
375 let _ = sqlx::query!(
376 "UPDATE blobs SET takedown_ref = $1 WHERE cid = $2",
377 takedown_ref,
378 cid
379 )
380 .execute(&state.db)
381 .await;
382 }
383
384 return (
385 StatusCode::OK,
386 Json(json!({
387 "subject": input.subject,
388 "takedown": input.takedown.as_ref().map(|t| json!({
389 "applied": t.apply,
390 "ref": t.r#ref
391 }))
392 })),
393 )
394 .into_response();
395 }
396 }
397 _ => {}
398 }
399
400 (
401 StatusCode::BAD_REQUEST,
402 Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),
403 )
404 .into_response()
405}
406
407#[derive(Deserialize)]
408pub struct GetInviteCodesParams {
409 pub sort: Option<String>,
410 pub limit: Option<i64>,
411 pub cursor: Option<String>,
412}
413
414#[derive(Serialize)]
415#[serde(rename_all = "camelCase")]
416pub struct InviteCodeInfo {
417 pub code: String,
418 pub available: i32,
419 pub disabled: bool,
420 pub for_account: String,
421 pub created_by: String,
422 pub created_at: String,
423 pub uses: Vec<InviteCodeUseInfo>,
424}
425
426#[derive(Serialize)]
427#[serde(rename_all = "camelCase")]
428pub struct InviteCodeUseInfo {
429 pub used_by: String,
430 pub used_at: String,
431}
432
433#[derive(Serialize)]
434pub struct GetInviteCodesOutput {
435 pub cursor: Option<String>,
436 pub codes: Vec<InviteCodeInfo>,
437}
438
439pub async fn get_invite_codes(
440 State(state): State<AppState>,
441 headers: axum::http::HeaderMap,
442 Query(params): Query<GetInviteCodesParams>,
443) -> Response {
444 let auth_header = headers.get("Authorization");
445 if auth_header.is_none() {
446 return (
447 StatusCode::UNAUTHORIZED,
448 Json(json!({"error": "AuthenticationRequired"})),
449 )
450 .into_response();
451 }
452
453 let limit = params.limit.unwrap_or(100).min(500);
454 let sort = params.sort.as_deref().unwrap_or("recent");
455
456 let order_clause = match sort {
457 "usage" => "available_uses DESC",
458 _ => "created_at DESC",
459 };
460
461 let codes_result = if let Some(cursor) = ¶ms.cursor {
462 sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!(
463 r#"
464 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
465 FROM invite_codes ic
466 WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1)
467 ORDER BY {}
468 LIMIT $2
469 "#,
470 order_clause
471 ))
472 .bind(cursor)
473 .bind(limit)
474 .fetch_all(&state.db)
475 .await
476 } else {
477 sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!(
478 r#"
479 SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
480 FROM invite_codes ic
481 ORDER BY {}
482 LIMIT $1
483 "#,
484 order_clause
485 ))
486 .bind(limit)
487 .fetch_all(&state.db)
488 .await
489 };
490
491 let codes_rows = match codes_result {
492 Ok(rows) => rows,
493 Err(e) => {
494 error!("DB error fetching invite codes: {:?}", e);
495 return (
496 StatusCode::INTERNAL_SERVER_ERROR,
497 Json(json!({"error": "InternalError"})),
498 )
499 .into_response();
500 }
501 };
502
503 let mut codes = Vec::new();
504 for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows {
505 let creator_did = sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user)
506 .fetch_optional(&state.db)
507 .await
508 .ok()
509 .flatten()
510 .unwrap_or_else(|| "unknown".to_string());
511
512 let uses_result = sqlx::query!(
513 r#"
514 SELECT u.did, icu.used_at
515 FROM invite_code_uses icu
516 JOIN users u ON icu.used_by_user = u.id
517 WHERE icu.code = $1
518 ORDER BY icu.used_at DESC
519 "#,
520 code
521 )
522 .fetch_all(&state.db)
523 .await;
524
525 let uses = match uses_result {
526 Ok(use_rows) => use_rows
527 .iter()
528 .map(|u| InviteCodeUseInfo {
529 used_by: u.did.clone(),
530 used_at: u.used_at.to_rfc3339(),
531 })
532 .collect(),
533 Err(_) => Vec::new(),
534 };
535
536 codes.push(InviteCodeInfo {
537 code: code.clone(),
538 available: *available_uses,
539 disabled: disabled.unwrap_or(false),
540 for_account: creator_did.clone(),
541 created_by: creator_did,
542 created_at: created_at.to_rfc3339(),
543 uses,
544 });
545 }
546
547 let next_cursor = if codes_rows.len() == limit as usize {
548 codes_rows.last().map(|(code, _, _, _, _)| code.clone())
549 } else {
550 None
551 };
552
553 (
554 StatusCode::OK,
555 Json(GetInviteCodesOutput {
556 cursor: next_cursor,
557 codes,
558 }),
559 )
560 .into_response()
561}
562
563#[derive(Deserialize)]
564pub struct DisableAccountInvitesInput {
565 pub account: String,
566}
567
568pub async fn disable_account_invites(
569 State(state): State<AppState>,
570 headers: axum::http::HeaderMap,
571 Json(input): Json<DisableAccountInvitesInput>,
572) -> Response {
573 let auth_header = headers.get("Authorization");
574 if auth_header.is_none() {
575 return (
576 StatusCode::UNAUTHORIZED,
577 Json(json!({"error": "AuthenticationRequired"})),
578 )
579 .into_response();
580 }
581
582 let account = input.account.trim();
583 if account.is_empty() {
584 return (
585 StatusCode::BAD_REQUEST,
586 Json(json!({"error": "InvalidRequest", "message": "account is required"})),
587 )
588 .into_response();
589 }
590
591 let result = sqlx::query!("UPDATE users SET invites_disabled = TRUE WHERE did = $1", account)
592 .execute(&state.db)
593 .await;
594
595 match result {
596 Ok(r) => {
597 if r.rows_affected() == 0 {
598 return (
599 StatusCode::NOT_FOUND,
600 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
601 )
602 .into_response();
603 }
604 (StatusCode::OK, Json(json!({}))).into_response()
605 }
606 Err(e) => {
607 error!("DB error disabling account invites: {:?}", e);
608 (
609 StatusCode::INTERNAL_SERVER_ERROR,
610 Json(json!({"error": "InternalError"})),
611 )
612 .into_response()
613 }
614 }
615}
616
617#[derive(Deserialize)]
618pub struct EnableAccountInvitesInput {
619 pub account: String,
620}
621
622pub async fn enable_account_invites(
623 State(state): State<AppState>,
624 headers: axum::http::HeaderMap,
625 Json(input): Json<EnableAccountInvitesInput>,
626) -> Response {
627 let auth_header = headers.get("Authorization");
628 if auth_header.is_none() {
629 return (
630 StatusCode::UNAUTHORIZED,
631 Json(json!({"error": "AuthenticationRequired"})),
632 )
633 .into_response();
634 }
635
636 let account = input.account.trim();
637 if account.is_empty() {
638 return (
639 StatusCode::BAD_REQUEST,
640 Json(json!({"error": "InvalidRequest", "message": "account is required"})),
641 )
642 .into_response();
643 }
644
645 let result = sqlx::query!("UPDATE users SET invites_disabled = FALSE WHERE did = $1", account)
646 .execute(&state.db)
647 .await;
648
649 match result {
650 Ok(r) => {
651 if r.rows_affected() == 0 {
652 return (
653 StatusCode::NOT_FOUND,
654 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
655 )
656 .into_response();
657 }
658 (StatusCode::OK, Json(json!({}))).into_response()
659 }
660 Err(e) => {
661 error!("DB error enabling account invites: {:?}", e);
662 (
663 StatusCode::INTERNAL_SERVER_ERROR,
664 Json(json!({"error": "InternalError"})),
665 )
666 .into_response()
667 }
668 }
669}
670
671#[derive(Deserialize)]
672pub struct GetAccountInfoParams {
673 pub did: String,
674}
675
676#[derive(Serialize)]
677#[serde(rename_all = "camelCase")]
678pub struct AccountInfo {
679 pub did: String,
680 pub handle: String,
681 pub email: Option<String>,
682 pub indexed_at: String,
683 pub invite_note: Option<String>,
684 pub invites_disabled: bool,
685 pub email_confirmed_at: Option<String>,
686 pub deactivated_at: Option<String>,
687}
688
689#[derive(Serialize)]
690#[serde(rename_all = "camelCase")]
691pub struct GetAccountInfosOutput {
692 pub infos: Vec<AccountInfo>,
693}
694
695pub async fn get_account_info(
696 State(state): State<AppState>,
697 headers: axum::http::HeaderMap,
698 Query(params): Query<GetAccountInfoParams>,
699) -> Response {
700 let auth_header = headers.get("Authorization");
701 if auth_header.is_none() {
702 return (
703 StatusCode::UNAUTHORIZED,
704 Json(json!({"error": "AuthenticationRequired"})),
705 )
706 .into_response();
707 }
708
709 let did = params.did.trim();
710 if did.is_empty() {
711 return (
712 StatusCode::BAD_REQUEST,
713 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
714 )
715 .into_response();
716 }
717
718 let result = sqlx::query!(
719 r#"
720 SELECT did, handle, email, created_at
721 FROM users
722 WHERE did = $1
723 "#,
724 did
725 )
726 .fetch_optional(&state.db)
727 .await;
728
729 match result {
730 Ok(Some(row)) => {
731 (
732 StatusCode::OK,
733 Json(AccountInfo {
734 did: row.did,
735 handle: row.handle,
736 email: Some(row.email),
737 indexed_at: row.created_at.to_rfc3339(),
738 invite_note: None,
739 invites_disabled: false,
740 email_confirmed_at: None,
741 deactivated_at: None,
742 }),
743 )
744 .into_response()
745 }
746 Ok(None) => (
747 StatusCode::NOT_FOUND,
748 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
749 )
750 .into_response(),
751 Err(e) => {
752 error!("DB error in get_account_info: {:?}", e);
753 (
754 StatusCode::INTERNAL_SERVER_ERROR,
755 Json(json!({"error": "InternalError"})),
756 )
757 .into_response()
758 }
759 }
760}
761
762#[derive(Deserialize)]
763pub struct GetAccountInfosParams {
764 pub dids: String,
765}
766
767pub async fn get_account_infos(
768 State(state): State<AppState>,
769 headers: axum::http::HeaderMap,
770 Query(params): Query<GetAccountInfosParams>,
771) -> Response {
772 let auth_header = headers.get("Authorization");
773 if auth_header.is_none() {
774 return (
775 StatusCode::UNAUTHORIZED,
776 Json(json!({"error": "AuthenticationRequired"})),
777 )
778 .into_response();
779 }
780
781 let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect();
782 if dids.is_empty() {
783 return (
784 StatusCode::BAD_REQUEST,
785 Json(json!({"error": "InvalidRequest", "message": "dids is required"})),
786 )
787 .into_response();
788 }
789
790 let mut infos = Vec::new();
791
792 for did in dids {
793 if did.is_empty() {
794 continue;
795 }
796
797 let result = sqlx::query!(
798 r#"
799 SELECT did, handle, email, created_at
800 FROM users
801 WHERE did = $1
802 "#,
803 did
804 )
805 .fetch_optional(&state.db)
806 .await;
807
808 if let Ok(Some(row)) = result {
809 infos.push(AccountInfo {
810 did: row.did,
811 handle: row.handle,
812 email: Some(row.email),
813 indexed_at: row.created_at.to_rfc3339(),
814 invite_note: None,
815 invites_disabled: false,
816 email_confirmed_at: None,
817 deactivated_at: None,
818 });
819 }
820 }
821
822 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
823}
824
825#[derive(Deserialize)]
826pub struct DeleteAccountInput {
827 pub did: String,
828}
829
830pub async fn delete_account(
831 State(state): State<AppState>,
832 headers: axum::http::HeaderMap,
833 Json(input): Json<DeleteAccountInput>,
834) -> Response {
835 let auth_header = headers.get("Authorization");
836 if auth_header.is_none() {
837 return (
838 StatusCode::UNAUTHORIZED,
839 Json(json!({"error": "AuthenticationRequired"})),
840 )
841 .into_response();
842 }
843
844 let did = input.did.trim();
845 if did.is_empty() {
846 return (
847 StatusCode::BAD_REQUEST,
848 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
849 )
850 .into_response();
851 }
852
853 let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
854 .fetch_optional(&state.db)
855 .await;
856
857 let user_id = match user {
858 Ok(Some(row)) => row.id,
859 Ok(None) => {
860 return (
861 StatusCode::NOT_FOUND,
862 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
863 )
864 .into_response();
865 }
866 Err(e) => {
867 error!("DB error in delete_account: {:?}", e);
868 return (
869 StatusCode::INTERNAL_SERVER_ERROR,
870 Json(json!({"error": "InternalError"})),
871 )
872 .into_response();
873 }
874 };
875
876 let _ = sqlx::query!("DELETE FROM sessions WHERE did = $1", did)
877 .execute(&state.db)
878 .await;
879
880 let _ = sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
881 .execute(&state.db)
882 .await;
883
884 let _ = sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
885 .execute(&state.db)
886 .await;
887
888 let _ = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
889 .execute(&state.db)
890 .await;
891
892 let _ = sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
893 .execute(&state.db)
894 .await;
895
896 let result = sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
897 .execute(&state.db)
898 .await;
899
900 match result {
901 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
902 Err(e) => {
903 error!("DB error deleting account: {:?}", e);
904 (
905 StatusCode::INTERNAL_SERVER_ERROR,
906 Json(json!({"error": "InternalError"})),
907 )
908 .into_response()
909 }
910 }
911}
912
913#[derive(Deserialize)]
914pub struct UpdateAccountEmailInput {
915 pub account: String,
916 pub email: String,
917}
918
919pub async fn update_account_email(
920 State(state): State<AppState>,
921 headers: axum::http::HeaderMap,
922 Json(input): Json<UpdateAccountEmailInput>,
923) -> Response {
924 let auth_header = headers.get("Authorization");
925 if auth_header.is_none() {
926 return (
927 StatusCode::UNAUTHORIZED,
928 Json(json!({"error": "AuthenticationRequired"})),
929 )
930 .into_response();
931 }
932
933 let account = input.account.trim();
934 let email = input.email.trim();
935
936 if account.is_empty() || email.is_empty() {
937 return (
938 StatusCode::BAD_REQUEST,
939 Json(json!({"error": "InvalidRequest", "message": "account and email are required"})),
940 )
941 .into_response();
942 }
943
944 let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account)
945 .execute(&state.db)
946 .await;
947
948 match result {
949 Ok(r) => {
950 if r.rows_affected() == 0 {
951 return (
952 StatusCode::NOT_FOUND,
953 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
954 )
955 .into_response();
956 }
957 (StatusCode::OK, Json(json!({}))).into_response()
958 }
959 Err(e) => {
960 error!("DB error updating email: {:?}", e);
961 (
962 StatusCode::INTERNAL_SERVER_ERROR,
963 Json(json!({"error": "InternalError"})),
964 )
965 .into_response()
966 }
967 }
968}
969
970#[derive(Deserialize)]
971pub struct UpdateAccountHandleInput {
972 pub did: String,
973 pub handle: String,
974}
975
976pub async fn update_account_handle(
977 State(state): State<AppState>,
978 headers: axum::http::HeaderMap,
979 Json(input): Json<UpdateAccountHandleInput>,
980) -> Response {
981 let auth_header = headers.get("Authorization");
982 if auth_header.is_none() {
983 return (
984 StatusCode::UNAUTHORIZED,
985 Json(json!({"error": "AuthenticationRequired"})),
986 )
987 .into_response();
988 }
989
990 let did = input.did.trim();
991 let handle = input.handle.trim();
992
993 if did.is_empty() || handle.is_empty() {
994 return (
995 StatusCode::BAD_REQUEST,
996 Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})),
997 )
998 .into_response();
999 }
1000
1001 if !handle
1002 .chars()
1003 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
1004 {
1005 return (
1006 StatusCode::BAD_REQUEST,
1007 Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})),
1008 )
1009 .into_response();
1010 }
1011
1012 let existing = sqlx::query!("SELECT id FROM users WHERE handle = $1 AND did != $2", handle, did)
1013 .fetch_optional(&state.db)
1014 .await;
1015
1016 if let Ok(Some(_)) = existing {
1017 return (
1018 StatusCode::BAD_REQUEST,
1019 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})),
1020 )
1021 .into_response();
1022 }
1023
1024 let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did)
1025 .execute(&state.db)
1026 .await;
1027
1028 match result {
1029 Ok(r) => {
1030 if r.rows_affected() == 0 {
1031 return (
1032 StatusCode::NOT_FOUND,
1033 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
1034 )
1035 .into_response();
1036 }
1037 (StatusCode::OK, Json(json!({}))).into_response()
1038 }
1039 Err(e) => {
1040 error!("DB error updating handle: {:?}", e);
1041 (
1042 StatusCode::INTERNAL_SERVER_ERROR,
1043 Json(json!({"error": "InternalError"})),
1044 )
1045 .into_response()
1046 }
1047 }
1048}
1049
1050#[derive(Deserialize)]
1051pub struct UpdateAccountPasswordInput {
1052 pub did: String,
1053 pub password: String,
1054}
1055
1056pub async fn update_account_password(
1057 State(state): State<AppState>,
1058 headers: axum::http::HeaderMap,
1059 Json(input): Json<UpdateAccountPasswordInput>,
1060) -> Response {
1061 let auth_header = headers.get("Authorization");
1062 if auth_header.is_none() {
1063 return (
1064 StatusCode::UNAUTHORIZED,
1065 Json(json!({"error": "AuthenticationRequired"})),
1066 )
1067 .into_response();
1068 }
1069
1070 let did = input.did.trim();
1071 let password = input.password.trim();
1072
1073 if did.is_empty() || password.is_empty() {
1074 return (
1075 StatusCode::BAD_REQUEST,
1076 Json(json!({"error": "InvalidRequest", "message": "did and password are required"})),
1077 )
1078 .into_response();
1079 }
1080
1081 let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) {
1082 Ok(h) => h,
1083 Err(e) => {
1084 error!("Failed to hash password: {:?}", e);
1085 return (
1086 StatusCode::INTERNAL_SERVER_ERROR,
1087 Json(json!({"error": "InternalError"})),
1088 )
1089 .into_response();
1090 }
1091 };
1092
1093 let result = sqlx::query!("UPDATE users SET password_hash = $1 WHERE did = $2", password_hash, did)
1094 .execute(&state.db)
1095 .await;
1096
1097 match result {
1098 Ok(r) => {
1099 if r.rows_affected() == 0 {
1100 return (
1101 StatusCode::NOT_FOUND,
1102 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
1103 )
1104 .into_response();
1105 }
1106 (StatusCode::OK, Json(json!({}))).into_response()
1107 }
1108 Err(e) => {
1109 error!("DB error updating password: {:?}", e);
1110 (
1111 StatusCode::INTERNAL_SERVER_ERROR,
1112 Json(json!({"error": "InternalError"})),
1113 )
1114 .into_response()
1115 }
1116 }
1117}