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}