this repo has no description
1use crate::api::ApiError;
2use crate::plc::PlcClient;
3use crate::state::AppState;
4use axum::{
5 Json,
6 extract::State,
7 http::StatusCode,
8 response::{IntoResponse, Response},
9};
10use bcrypt::verify;
11use chrono::{Duration, Utc};
12use k256::ecdsa::SigningKey;
13use serde::{Deserialize, Serialize};
14use serde_json::json;
15use tracing::{error, info, warn};
16use uuid::Uuid;
17
18#[derive(Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct CheckAccountStatusOutput {
21 pub activated: bool,
22 pub valid_did: bool,
23 pub repo_commit: String,
24 pub repo_rev: String,
25 pub repo_blocks: i64,
26 pub indexed_records: i64,
27 pub private_state_values: i64,
28 pub expected_blobs: i64,
29 pub imported_blobs: i64,
30}
31
32pub async fn check_account_status(
33 State(state): State<AppState>,
34 headers: axum::http::HeaderMap,
35) -> Response {
36 let extracted = match crate::auth::extract_auth_token_from_header(
37 headers.get("Authorization").and_then(|h| h.to_str().ok()),
38 ) {
39 Some(t) => t,
40 None => return ApiError::AuthenticationRequired.into_response(),
41 };
42 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
43 let http_uri = format!(
44 "https://{}/xrpc/com.atproto.server.checkAccountStatus",
45 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
46 );
47 let did = match crate::auth::validate_token_with_dpop(
48 &state.db,
49 &extracted.token,
50 extracted.is_dpop,
51 dpop_proof,
52 "GET",
53 &http_uri,
54 true,
55 )
56 .await
57 {
58 Ok(user) => user.did,
59 Err(e) => return ApiError::from(e).into_response(),
60 };
61 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
62 .fetch_optional(&state.db)
63 .await
64 {
65 Ok(Some(id)) => id,
66 _ => {
67 return (
68 StatusCode::INTERNAL_SERVER_ERROR,
69 Json(json!({"error": "InternalError"})),
70 )
71 .into_response();
72 }
73 };
74 let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
75 .fetch_optional(&state.db)
76 .await;
77 let deactivated_at = match user_status {
78 Ok(Some(row)) => row.deactivated_at,
79 _ => None,
80 };
81 let repo_result = sqlx::query!(
82 "SELECT repo_root_cid FROM repos WHERE user_id = $1",
83 user_id
84 )
85 .fetch_optional(&state.db)
86 .await;
87 let repo_commit = match repo_result {
88 Ok(Some(row)) => row.repo_root_cid,
89 _ => String::new(),
90 };
91 let record_count: i64 =
92 sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
93 .fetch_one(&state.db)
94 .await
95 .unwrap_or(Some(0))
96 .unwrap_or(0);
97 let blob_count: i64 = sqlx::query_scalar!(
98 "SELECT COUNT(*) FROM blobs WHERE created_by_user = $1",
99 user_id
100 )
101 .fetch_one(&state.db)
102 .await
103 .unwrap_or(Some(0))
104 .unwrap_or(0);
105 let valid_did = did.starts_with("did:");
106 (
107 StatusCode::OK,
108 Json(CheckAccountStatusOutput {
109 activated: deactivated_at.is_none(),
110 valid_did,
111 repo_commit: repo_commit.clone(),
112 repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
113 repo_blocks: 0,
114 indexed_records: record_count,
115 private_state_values: 0,
116 expected_blobs: blob_count,
117 imported_blobs: blob_count,
118 }),
119 )
120 .into_response()
121}
122
123async fn assert_valid_did_document_for_service(
124 db: &sqlx::PgPool,
125 did: &str,
126) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
127 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
128 let expected_endpoint = format!("https://{}", hostname);
129
130 if did.starts_with("did:plc:") {
131 let plc_client = PlcClient::new(None);
132
133 let mut last_error = None;
134 let mut doc_data = None;
135 for attempt in 0..5 {
136 if attempt > 0 {
137 let delay_ms = 500 * (1 << (attempt - 1));
138 info!(
139 "Waiting {}ms before retry {} for DID document validation ({})",
140 delay_ms, attempt, did
141 );
142 tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
143 }
144
145 match plc_client.get_document_data(did).await {
146 Ok(data) => {
147 let pds_endpoint = data
148 .get("services")
149 .and_then(|s| s.get("atproto_pds").or_else(|| s.get("atprotoPds")))
150 .and_then(|p| p.get("endpoint"))
151 .and_then(|e| e.as_str());
152
153 if pds_endpoint == Some(&expected_endpoint) {
154 doc_data = Some(data);
155 break;
156 } else {
157 info!(
158 "Attempt {}: DID {} has endpoint {:?}, expected {} - retrying",
159 attempt + 1,
160 did,
161 pds_endpoint,
162 expected_endpoint
163 );
164 last_error = Some(format!(
165 "DID document endpoint {:?} does not match expected {}",
166 pds_endpoint, expected_endpoint
167 ));
168 }
169 }
170 Err(e) => {
171 warn!(
172 "Attempt {}: Failed to fetch PLC document for {}: {:?}",
173 attempt + 1,
174 did,
175 e
176 );
177 last_error = Some(format!("Could not resolve DID document: {}", e));
178 }
179 }
180 }
181
182 let doc_data = match doc_data {
183 Some(d) => d,
184 None => {
185 return Err((
186 StatusCode::BAD_REQUEST,
187 Json(json!({
188 "error": "InvalidRequest",
189 "message": last_error.unwrap_or_else(|| "DID document validation failed".to_string())
190 })),
191 ));
192 }
193 };
194
195 let doc_signing_key = doc_data
196 .get("verificationMethods")
197 .and_then(|v| v.get("atproto"))
198 .and_then(|k| k.as_str());
199
200 let user_row = sqlx::query!(
201 "SELECT uk.key_bytes, uk.encryption_version FROM user_keys uk JOIN users u ON uk.user_id = u.id WHERE u.did = $1",
202 did
203 )
204 .fetch_optional(db)
205 .await
206 .map_err(|e| {
207 error!("Failed to fetch user key: {:?}", e);
208 (
209 StatusCode::INTERNAL_SERVER_ERROR,
210 Json(json!({"error": "InternalError"})),
211 )
212 })?;
213
214 if let Some(row) = user_row {
215 let key_bytes =
216 crate::config::decrypt_key(&row.key_bytes, row.encryption_version).map_err(|e| {
217 error!("Failed to decrypt user key: {}", e);
218 (
219 StatusCode::INTERNAL_SERVER_ERROR,
220 Json(json!({"error": "InternalError"})),
221 )
222 })?;
223 let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| {
224 error!("Failed to create signing key: {:?}", e);
225 (
226 StatusCode::INTERNAL_SERVER_ERROR,
227 Json(json!({"error": "InternalError"})),
228 )
229 })?;
230 let expected_did_key = crate::plc::signing_key_to_did_key(&signing_key);
231
232 if doc_signing_key != Some(&expected_did_key) {
233 warn!(
234 "DID {} has signing key {:?}, expected {}",
235 did, doc_signing_key, expected_did_key
236 );
237 return Err((
238 StatusCode::BAD_REQUEST,
239 Json(json!({
240 "error": "InvalidRequest",
241 "message": "DID document verification method does not match expected signing key"
242 })),
243 ));
244 }
245 }
246 } else if did.starts_with("did:web:") {
247 let client = reqwest::Client::new();
248 let did_path = &did[8..];
249 let url = format!("https://{}/.well-known/did.json", did_path.replace(':', "/"));
250 let resp = client.get(&url).send().await.map_err(|e| {
251 warn!("Failed to fetch did:web document for {}: {:?}", did, e);
252 (
253 StatusCode::BAD_REQUEST,
254 Json(json!({
255 "error": "InvalidRequest",
256 "message": format!("Could not resolve DID document: {}", e)
257 })),
258 )
259 })?;
260 let doc: serde_json::Value = resp.json().await.map_err(|e| {
261 warn!("Failed to parse did:web document for {}: {:?}", did, e);
262 (
263 StatusCode::BAD_REQUEST,
264 Json(json!({
265 "error": "InvalidRequest",
266 "message": format!("Could not parse DID document: {}", e)
267 })),
268 )
269 })?;
270
271 let pds_endpoint = doc
272 .get("service")
273 .and_then(|s| s.as_array())
274 .and_then(|arr| {
275 arr.iter().find(|svc| {
276 svc.get("id").and_then(|id| id.as_str()) == Some("#atproto_pds")
277 || svc.get("type").and_then(|t| t.as_str())
278 == Some("AtprotoPersonalDataServer")
279 })
280 })
281 .and_then(|svc| svc.get("serviceEndpoint"))
282 .and_then(|e| e.as_str());
283
284 if pds_endpoint != Some(&expected_endpoint) {
285 warn!(
286 "DID {} has endpoint {:?}, expected {}",
287 did, pds_endpoint, expected_endpoint
288 );
289 return Err((
290 StatusCode::BAD_REQUEST,
291 Json(json!({
292 "error": "InvalidRequest",
293 "message": "DID document atproto_pds service endpoint does not match PDS public url"
294 })),
295 ));
296 }
297 }
298
299 Ok(())
300}
301
302pub async fn activate_account(
303 State(state): State<AppState>,
304 headers: axum::http::HeaderMap,
305) -> Response {
306 let extracted = match crate::auth::extract_auth_token_from_header(
307 headers.get("Authorization").and_then(|h| h.to_str().ok()),
308 ) {
309 Some(t) => t,
310 None => return ApiError::AuthenticationRequired.into_response(),
311 };
312 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
313 let http_uri = format!(
314 "https://{}/xrpc/com.atproto.server.activateAccount",
315 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
316 );
317 let auth_user = match crate::auth::validate_token_with_dpop(
318 &state.db,
319 &extracted.token,
320 extracted.is_dpop,
321 dpop_proof,
322 "POST",
323 &http_uri,
324 true,
325 )
326 .await
327 {
328 Ok(user) => user,
329 Err(e) => return ApiError::from(e).into_response(),
330 };
331
332 if let Err(e) = crate::auth::scope_check::check_account_scope(
333 auth_user.is_oauth,
334 auth_user.scope.as_deref(),
335 crate::oauth::scopes::AccountAttr::Repo,
336 crate::oauth::scopes::AccountAction::Manage,
337 ) {
338 return e;
339 }
340
341 let did = auth_user.did;
342
343 if let Err((status, json)) = assert_valid_did_document_for_service(&state.db, &did).await {
344 info!(
345 "activateAccount rejected for {}: DID document validation failed",
346 did
347 );
348 return (status, json).into_response();
349 }
350
351 let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
352 .fetch_optional(&state.db)
353 .await
354 .ok()
355 .flatten();
356 let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
357 .execute(&state.db)
358 .await;
359 match result {
360 Ok(_) => {
361 if let Some(ref h) = handle {
362 let _ = state.cache.delete(&format!("handle:{}", h)).await;
363 }
364 if let Err(e) =
365 crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
366 {
367 warn!("Failed to sequence account activation event: {}", e);
368 }
369 if let Err(e) =
370 crate::api::repo::record::sequence_identity_event(&state, &did, handle.as_deref())
371 .await
372 {
373 warn!("Failed to sequence identity event for activation: {}", e);
374 }
375 let repo_root = sqlx::query_scalar!(
376 "SELECT r.repo_root_cid FROM repos r JOIN users u ON r.user_id = u.id WHERE u.did = $1",
377 did
378 )
379 .fetch_optional(&state.db)
380 .await
381 .ok()
382 .flatten();
383 if let Some(root_cid) = repo_root {
384 if let Err(e) =
385 crate::api::repo::record::sequence_sync_event(&state, &did, &root_cid).await
386 {
387 warn!("Failed to sequence sync event for activation: {}", e);
388 }
389 }
390 (StatusCode::OK, Json(json!({}))).into_response()
391 }
392 Err(e) => {
393 error!("DB error activating account: {:?}", e);
394 (
395 StatusCode::INTERNAL_SERVER_ERROR,
396 Json(json!({"error": "InternalError"})),
397 )
398 .into_response()
399 }
400 }
401}
402
403#[derive(Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct DeactivateAccountInput {
406 pub delete_after: Option<String>,
407}
408
409pub async fn deactivate_account(
410 State(state): State<AppState>,
411 headers: axum::http::HeaderMap,
412 Json(_input): Json<DeactivateAccountInput>,
413) -> Response {
414 let extracted = match crate::auth::extract_auth_token_from_header(
415 headers.get("Authorization").and_then(|h| h.to_str().ok()),
416 ) {
417 Some(t) => t,
418 None => return ApiError::AuthenticationRequired.into_response(),
419 };
420 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
421 let http_uri = format!(
422 "https://{}/xrpc/com.atproto.server.deactivateAccount",
423 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
424 );
425 let auth_user = match crate::auth::validate_token_with_dpop(
426 &state.db,
427 &extracted.token,
428 extracted.is_dpop,
429 dpop_proof,
430 "POST",
431 &http_uri,
432 false,
433 )
434 .await
435 {
436 Ok(user) => user,
437 Err(e) => return ApiError::from(e).into_response(),
438 };
439
440 if let Err(e) = crate::auth::scope_check::check_account_scope(
441 auth_user.is_oauth,
442 auth_user.scope.as_deref(),
443 crate::oauth::scopes::AccountAttr::Repo,
444 crate::oauth::scopes::AccountAction::Manage,
445 ) {
446 return e;
447 }
448
449 let did = auth_user.did;
450 let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
451 .fetch_optional(&state.db)
452 .await
453 .ok()
454 .flatten();
455 let result = sqlx::query!(
456 "UPDATE users SET deactivated_at = NOW() WHERE did = $1",
457 did
458 )
459 .execute(&state.db)
460 .await;
461 match result {
462 Ok(_) => {
463 if let Some(ref h) = handle {
464 let _ = state.cache.delete(&format!("handle:{}", h)).await;
465 }
466 if let Err(e) = crate::api::repo::record::sequence_account_event(
467 &state,
468 &did,
469 false,
470 Some("deactivated"),
471 )
472 .await
473 {
474 warn!("Failed to sequence account deactivation event: {}", e);
475 }
476 (StatusCode::OK, Json(json!({}))).into_response()
477 }
478 Err(e) => {
479 error!("DB error deactivating account: {:?}", e);
480 (
481 StatusCode::INTERNAL_SERVER_ERROR,
482 Json(json!({"error": "InternalError"})),
483 )
484 .into_response()
485 }
486 }
487}
488
489pub async fn request_account_delete(
490 State(state): State<AppState>,
491 headers: axum::http::HeaderMap,
492) -> Response {
493 let extracted = match crate::auth::extract_auth_token_from_header(
494 headers.get("Authorization").and_then(|h| h.to_str().ok()),
495 ) {
496 Some(t) => t,
497 None => return ApiError::AuthenticationRequired.into_response(),
498 };
499 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
500 let http_uri = format!(
501 "https://{}/xrpc/com.atproto.server.requestAccountDelete",
502 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
503 );
504 let validated = match crate::auth::validate_token_with_dpop(
505 &state.db,
506 &extracted.token,
507 extracted.is_dpop,
508 dpop_proof,
509 "POST",
510 &http_uri,
511 true,
512 )
513 .await
514 {
515 Ok(user) => user,
516 Err(e) => return ApiError::from(e).into_response(),
517 };
518 let did = validated.did.clone();
519
520 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &did).await {
521 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &did).await;
522 }
523
524 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
525 .fetch_optional(&state.db)
526 .await
527 {
528 Ok(Some(id)) => id,
529 _ => {
530 return (
531 StatusCode::INTERNAL_SERVER_ERROR,
532 Json(json!({"error": "InternalError"})),
533 )
534 .into_response();
535 }
536 };
537 let confirmation_token = Uuid::new_v4().to_string();
538 let expires_at = Utc::now() + Duration::minutes(15);
539 let insert = sqlx::query!(
540 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
541 confirmation_token,
542 did,
543 expires_at
544 )
545 .execute(&state.db)
546 .await;
547 if let Err(e) = insert {
548 error!("DB error creating deletion token: {:?}", e);
549 return (
550 StatusCode::INTERNAL_SERVER_ERROR,
551 Json(json!({"error": "InternalError"})),
552 )
553 .into_response();
554 }
555 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
556 if let Err(e) =
557 crate::comms::enqueue_account_deletion(&state.db, user_id, &confirmation_token, &hostname)
558 .await
559 {
560 warn!("Failed to enqueue account deletion notification: {:?}", e);
561 }
562 info!("Account deletion requested for user {}", did);
563 (StatusCode::OK, Json(json!({}))).into_response()
564}
565
566#[derive(Deserialize)]
567pub struct DeleteAccountInput {
568 pub did: String,
569 pub password: String,
570 pub token: String,
571}
572
573pub async fn delete_account(
574 State(state): State<AppState>,
575 Json(input): Json<DeleteAccountInput>,
576) -> Response {
577 let did = input.did.trim();
578 let password = &input.password;
579 let token = input.token.trim();
580 if did.is_empty() {
581 return (
582 StatusCode::BAD_REQUEST,
583 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
584 )
585 .into_response();
586 }
587 if password.is_empty() {
588 return (
589 StatusCode::BAD_REQUEST,
590 Json(json!({"error": "InvalidRequest", "message": "password is required"})),
591 )
592 .into_response();
593 }
594 if token.is_empty() {
595 return (
596 StatusCode::BAD_REQUEST,
597 Json(json!({"error": "InvalidToken", "message": "token is required"})),
598 )
599 .into_response();
600 }
601 let user = sqlx::query!(
602 "SELECT id, password_hash, handle FROM users WHERE did = $1",
603 did
604 )
605 .fetch_optional(&state.db)
606 .await;
607 let (user_id, password_hash, handle) = match user {
608 Ok(Some(row)) => (row.id, row.password_hash, row.handle),
609 Ok(None) => {
610 return (
611 StatusCode::BAD_REQUEST,
612 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
613 )
614 .into_response();
615 }
616 Err(e) => {
617 error!("DB error in delete_account: {:?}", e);
618 return (
619 StatusCode::INTERNAL_SERVER_ERROR,
620 Json(json!({"error": "InternalError"})),
621 )
622 .into_response();
623 }
624 };
625 let password_valid = if password_hash
626 .as_ref()
627 .map(|h| verify(password, h).unwrap_or(false))
628 .unwrap_or(false)
629 {
630 true
631 } else {
632 let app_pass_rows = sqlx::query!(
633 "SELECT password_hash FROM app_passwords WHERE user_id = $1",
634 user_id
635 )
636 .fetch_all(&state.db)
637 .await
638 .unwrap_or_default();
639 app_pass_rows
640 .iter()
641 .any(|row| verify(password, &row.password_hash).unwrap_or(false))
642 };
643 if !password_valid {
644 return (
645 StatusCode::UNAUTHORIZED,
646 Json(json!({"error": "AuthenticationFailed", "message": "Invalid password"})),
647 )
648 .into_response();
649 }
650 let deletion_request = sqlx::query!(
651 "SELECT did, expires_at FROM account_deletion_requests WHERE token = $1",
652 token
653 )
654 .fetch_optional(&state.db)
655 .await;
656 let (token_did, expires_at) = match deletion_request {
657 Ok(Some(row)) => (row.did, row.expires_at),
658 Ok(None) => {
659 return (
660 StatusCode::BAD_REQUEST,
661 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
662 )
663 .into_response();
664 }
665 Err(e) => {
666 error!("DB error fetching deletion token: {:?}", e);
667 return (
668 StatusCode::INTERNAL_SERVER_ERROR,
669 Json(json!({"error": "InternalError"})),
670 )
671 .into_response();
672 }
673 };
674 if token_did != did {
675 return (
676 StatusCode::BAD_REQUEST,
677 Json(json!({"error": "InvalidToken", "message": "Token does not match account"})),
678 )
679 .into_response();
680 }
681 if Utc::now() > expires_at {
682 let _ = sqlx::query!(
683 "DELETE FROM account_deletion_requests WHERE token = $1",
684 token
685 )
686 .execute(&state.db)
687 .await;
688 return (
689 StatusCode::BAD_REQUEST,
690 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
691 )
692 .into_response();
693 }
694 let mut tx = match state.db.begin().await {
695 Ok(tx) => tx,
696 Err(e) => {
697 error!("Failed to begin transaction: {:?}", e);
698 return (
699 StatusCode::INTERNAL_SERVER_ERROR,
700 Json(json!({"error": "InternalError"})),
701 )
702 .into_response();
703 }
704 };
705 let deletion_result: Result<(), sqlx::Error> = async {
706 sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did)
707 .execute(&mut *tx)
708 .await?;
709 sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
710 .execute(&mut *tx)
711 .await?;
712 sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
713 .execute(&mut *tx)
714 .await?;
715 sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
716 .execute(&mut *tx)
717 .await?;
718 sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
719 .execute(&mut *tx)
720 .await?;
721 sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id)
722 .execute(&mut *tx)
723 .await?;
724 sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did)
725 .execute(&mut *tx)
726 .await?;
727 sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
728 .execute(&mut *tx)
729 .await?;
730 Ok(())
731 }
732 .await;
733 match deletion_result {
734 Ok(()) => {
735 if let Err(e) = tx.commit().await {
736 error!("Failed to commit account deletion transaction: {:?}", e);
737 return (
738 StatusCode::INTERNAL_SERVER_ERROR,
739 Json(json!({"error": "InternalError"})),
740 )
741 .into_response();
742 }
743 if let Err(e) = crate::api::repo::record::sequence_account_event(
744 &state,
745 did,
746 false,
747 Some("deleted"),
748 )
749 .await
750 {
751 warn!(
752 "Failed to sequence account deletion event for {}: {}",
753 did, e
754 );
755 }
756 let _ = state.cache.delete(&format!("handle:{}", handle)).await;
757 info!("Account {} deleted successfully", did);
758 (StatusCode::OK, Json(json!({}))).into_response()
759 }
760 Err(e) => {
761 error!("DB error deleting account, rolling back: {:?}", e);
762 (
763 StatusCode::INTERNAL_SERVER_ERROR,
764 Json(json!({"error": "InternalError"})),
765 )
766 .into_response()
767 }
768 }
769}