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