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}