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, warn}; 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) = &params.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) = &params.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) = &params.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) = &params.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} 1118 1119#[derive(Deserialize)] 1120#[serde(rename_all = "camelCase")] 1121pub struct SendEmailInput { 1122 pub recipient_did: String, 1123 pub sender_did: String, 1124 pub content: String, 1125 pub subject: Option<String>, 1126 pub comment: Option<String>, 1127} 1128 1129#[derive(Serialize)] 1130pub struct SendEmailOutput { 1131 pub sent: bool, 1132} 1133 1134pub async fn send_email( 1135 State(state): State<AppState>, 1136 headers: axum::http::HeaderMap, 1137 Json(input): Json<SendEmailInput>, 1138) -> Response { 1139 let auth_header = headers.get("Authorization"); 1140 if auth_header.is_none() { 1141 return ( 1142 StatusCode::UNAUTHORIZED, 1143 Json(json!({"error": "AuthenticationRequired"})), 1144 ) 1145 .into_response(); 1146 } 1147 1148 let recipient_did = input.recipient_did.trim(); 1149 let content = input.content.trim(); 1150 1151 if recipient_did.is_empty() { 1152 return ( 1153 StatusCode::BAD_REQUEST, 1154 Json(json!({"error": "InvalidRequest", "message": "recipientDid is required"})), 1155 ) 1156 .into_response(); 1157 } 1158 1159 if content.is_empty() { 1160 return ( 1161 StatusCode::BAD_REQUEST, 1162 Json(json!({"error": "InvalidRequest", "message": "content is required"})), 1163 ) 1164 .into_response(); 1165 } 1166 1167 let user = sqlx::query!( 1168 "SELECT id, email, handle FROM users WHERE did = $1", 1169 recipient_did 1170 ) 1171 .fetch_optional(&state.db) 1172 .await; 1173 1174 let (user_id, email, handle) = match user { 1175 Ok(Some(row)) => (row.id, row.email, row.handle), 1176 Ok(None) => { 1177 return ( 1178 StatusCode::NOT_FOUND, 1179 Json(json!({"error": "AccountNotFound", "message": "Recipient account not found"})), 1180 ) 1181 .into_response(); 1182 } 1183 Err(e) => { 1184 error!("DB error in send_email: {:?}", e); 1185 return ( 1186 StatusCode::INTERNAL_SERVER_ERROR, 1187 Json(json!({"error": "InternalError"})), 1188 ) 1189 .into_response(); 1190 } 1191 }; 1192 1193 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1194 let subject = input 1195 .subject 1196 .clone() 1197 .unwrap_or_else(|| format!("Message from {}", hostname)); 1198 1199 let notification = crate::notifications::NewNotification::email( 1200 user_id, 1201 crate::notifications::NotificationType::AdminEmail, 1202 email, 1203 subject, 1204 content.to_string(), 1205 ); 1206 1207 let result = crate::notifications::enqueue_notification(&state.db, notification).await; 1208 1209 match result { 1210 Ok(_) => { 1211 tracing::info!( 1212 "Admin email queued for {} ({})", 1213 handle, 1214 recipient_did 1215 ); 1216 (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 1217 } 1218 Err(e) => { 1219 warn!("Failed to enqueue admin email: {:?}", e); 1220 (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() 1221 } 1222 } 1223}