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 bcrypt::verify; 9use chrono::{Duration, Utc}; 10use uuid::Uuid; 11use serde::{Deserialize, Serialize}; 12use serde_json::json; 13use tracing::{error, info, warn}; 14 15#[derive(Deserialize)] 16pub struct GetServiceAuthParams { 17 pub aud: String, 18 pub lxm: Option<String>, 19 pub exp: Option<i64>, 20} 21 22#[derive(Serialize)] 23pub struct GetServiceAuthOutput { 24 pub token: String, 25} 26 27pub async fn get_service_auth( 28 State(state): State<AppState>, 29 headers: axum::http::HeaderMap, 30 Query(params): Query<GetServiceAuthParams>, 31) -> Response { 32 let auth_header = headers.get("Authorization"); 33 if auth_header.is_none() { 34 return ( 35 StatusCode::UNAUTHORIZED, 36 Json(json!({"error": "AuthenticationRequired"})), 37 ) 38 .into_response(); 39 } 40 41 let token = auth_header 42 .unwrap() 43 .to_str() 44 .unwrap_or("") 45 .replace("Bearer ", ""); 46 47 let session = sqlx::query!( 48 r#" 49 SELECT s.did, k.key_bytes 50 FROM sessions s 51 JOIN users u ON s.did = u.did 52 JOIN user_keys k ON u.id = k.user_id 53 WHERE s.access_jwt = $1 54 "#, 55 token 56 ) 57 .fetch_optional(&state.db) 58 .await; 59 60 let (did, key_bytes) = match session { 61 Ok(Some(row)) => (row.did, row.key_bytes), 62 Ok(None) => { 63 return ( 64 StatusCode::UNAUTHORIZED, 65 Json(json!({"error": "AuthenticationFailed"})), 66 ) 67 .into_response(); 68 } 69 Err(e) => { 70 error!("DB error in get_service_auth: {:?}", e); 71 return ( 72 StatusCode::INTERNAL_SERVER_ERROR, 73 Json(json!({"error": "InternalError"})), 74 ) 75 .into_response(); 76 } 77 }; 78 79 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 80 return ( 81 StatusCode::UNAUTHORIZED, 82 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 83 ) 84 .into_response(); 85 } 86 87 let lxm = params.lxm.as_deref().unwrap_or("*"); 88 89 let service_token = match crate::auth::create_service_token(&did, &params.aud, lxm, &key_bytes) 90 { 91 Ok(t) => t, 92 Err(e) => { 93 error!("Failed to create service token: {:?}", e); 94 return ( 95 StatusCode::INTERNAL_SERVER_ERROR, 96 Json(json!({"error": "InternalError"})), 97 ) 98 .into_response(); 99 } 100 }; 101 102 (StatusCode::OK, Json(GetServiceAuthOutput { token: service_token })).into_response() 103} 104 105#[derive(Deserialize)] 106pub struct CreateSessionInput { 107 pub identifier: String, 108 pub password: String, 109} 110 111#[derive(Serialize)] 112#[serde(rename_all = "camelCase")] 113pub struct CreateSessionOutput { 114 pub access_jwt: String, 115 pub refresh_jwt: String, 116 pub handle: String, 117 pub did: String, 118} 119 120pub async fn create_session( 121 State(state): State<AppState>, 122 Json(input): Json<CreateSessionInput>, 123) -> Response { 124 info!("create_session: identifier='{}'", input.identifier); 125 126 let user_row = sqlx::query!( 127 "SELECT u.id, u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1", 128 input.identifier 129 ) 130 .fetch_optional(&state.db) 131 .await; 132 133 match user_row { 134 Ok(Some(row)) => { 135 let user_id = row.id; 136 let stored_hash = &row.password_hash; 137 let did = &row.did; 138 let handle = &row.handle; 139 let key_bytes = &row.key_bytes; 140 141 let password_valid = if verify(&input.password, stored_hash).unwrap_or(false) { 142 true 143 } else { 144 let app_pass_rows = sqlx::query!("SELECT password_hash FROM app_passwords WHERE user_id = $1", user_id) 145 .fetch_all(&state.db) 146 .await 147 .unwrap_or_default(); 148 149 app_pass_rows.iter().any(|row| { 150 verify(&input.password, &row.password_hash).unwrap_or(false) 151 }) 152 }; 153 154 if password_valid { 155 let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) { 156 Ok(t) => t, 157 Err(e) => { 158 error!("Failed to create access token: {:?}", e); 159 return ( 160 StatusCode::INTERNAL_SERVER_ERROR, 161 Json(json!({"error": "InternalError"})), 162 ) 163 .into_response(); 164 } 165 }; 166 167 let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) { 168 Ok(t) => t, 169 Err(e) => { 170 error!("Failed to create refresh token: {:?}", e); 171 return ( 172 StatusCode::INTERNAL_SERVER_ERROR, 173 Json(json!({"error": "InternalError"})), 174 ) 175 .into_response(); 176 } 177 }; 178 179 let session_insert = sqlx::query!( 180 "INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)", 181 access_jwt, 182 refresh_jwt, 183 did 184 ) 185 .execute(&state.db) 186 .await; 187 188 match session_insert { 189 Ok(_) => { 190 return ( 191 StatusCode::OK, 192 Json(CreateSessionOutput { 193 access_jwt, 194 refresh_jwt, 195 handle: handle.clone(), 196 did: did.clone(), 197 }), 198 ) 199 .into_response(); 200 } 201 Err(e) => { 202 error!("Failed to insert session: {:?}", e); 203 return ( 204 StatusCode::INTERNAL_SERVER_ERROR, 205 Json(json!({"error": "InternalError"})), 206 ) 207 .into_response(); 208 } 209 } 210 } else { 211 warn!( 212 "Password verification failed for identifier: {}", 213 input.identifier 214 ); 215 } 216 } 217 Ok(None) => { 218 warn!("User not found for identifier: {}", input.identifier); 219 } 220 Err(e) => { 221 error!("Database error fetching user: {:?}", e); 222 return ( 223 StatusCode::INTERNAL_SERVER_ERROR, 224 Json(json!({"error": "InternalError"})), 225 ) 226 .into_response(); 227 } 228 } 229 230 ( 231 StatusCode::UNAUTHORIZED, 232 Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"})), 233 ) 234 .into_response() 235} 236 237pub async fn get_session( 238 State(state): State<AppState>, 239 headers: axum::http::HeaderMap, 240) -> Response { 241 let auth_header = headers.get("Authorization"); 242 if auth_header.is_none() { 243 return ( 244 StatusCode::UNAUTHORIZED, 245 Json(json!({"error": "AuthenticationRequired"})), 246 ) 247 .into_response(); 248 } 249 250 let token = auth_header 251 .unwrap() 252 .to_str() 253 .unwrap_or("") 254 .replace("Bearer ", ""); 255 256 let result = sqlx::query!( 257 r#" 258 SELECT u.handle, u.did, u.email, k.key_bytes 259 FROM sessions s 260 JOIN users u ON s.did = u.did 261 JOIN user_keys k ON u.id = k.user_id 262 WHERE s.access_jwt = $1 263 "#, 264 token 265 ) 266 .fetch_optional(&state.db) 267 .await; 268 269 match result { 270 Ok(Some(row)) => { 271 if let Err(_) = crate::auth::verify_token(&token, &row.key_bytes) { 272 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response(); 273 } 274 275 return ( 276 StatusCode::OK, 277 Json(json!({ 278 "handle": row.handle, 279 "did": row.did, 280 "email": row.email, 281 "didDoc": {} 282 })), 283 ) 284 .into_response(); 285 } 286 Ok(None) => { 287 return ( 288 StatusCode::UNAUTHORIZED, 289 Json(json!({"error": "AuthenticationFailed"})), 290 ) 291 .into_response(); 292 } 293 Err(e) => { 294 error!("Database error in get_session: {:?}", e); 295 return ( 296 StatusCode::INTERNAL_SERVER_ERROR, 297 Json(json!({"error": "InternalError"})), 298 ) 299 .into_response(); 300 } 301 } 302} 303 304pub async fn delete_session( 305 State(state): State<AppState>, 306 headers: axum::http::HeaderMap, 307) -> Response { 308 let auth_header = headers.get("Authorization"); 309 if auth_header.is_none() { 310 return ( 311 StatusCode::UNAUTHORIZED, 312 Json(json!({"error": "AuthenticationRequired"})), 313 ) 314 .into_response(); 315 } 316 317 let token = auth_header 318 .unwrap() 319 .to_str() 320 .unwrap_or("") 321 .replace("Bearer ", ""); 322 323 let result = sqlx::query!("DELETE FROM sessions WHERE access_jwt = $1", token) 324 .execute(&state.db) 325 .await; 326 327 match result { 328 Ok(res) => { 329 if res.rows_affected() > 0 { 330 return (StatusCode::OK, Json(json!({}))).into_response(); 331 } 332 } 333 Err(e) => { 334 error!("Database error in delete_session: {:?}", e); 335 } 336 } 337 338 ( 339 StatusCode::UNAUTHORIZED, 340 Json(json!({"error": "AuthenticationFailed"})), 341 ) 342 .into_response() 343} 344 345pub async fn request_account_delete( 346 State(state): State<AppState>, 347 headers: axum::http::HeaderMap, 348) -> Response { 349 let auth_header = headers.get("Authorization"); 350 if auth_header.is_none() { 351 return ( 352 StatusCode::UNAUTHORIZED, 353 Json(json!({"error": "AuthenticationRequired"})), 354 ) 355 .into_response(); 356 } 357 358 let token = auth_header 359 .unwrap() 360 .to_str() 361 .unwrap_or("") 362 .replace("Bearer ", ""); 363 364 let session = sqlx::query!( 365 r#" 366 SELECT s.did, k.key_bytes 367 FROM sessions s 368 JOIN users u ON s.did = u.did 369 JOIN user_keys k ON u.id = k.user_id 370 WHERE s.access_jwt = $1 371 "#, 372 token 373 ) 374 .fetch_optional(&state.db) 375 .await; 376 377 let (did, key_bytes) = match session { 378 Ok(Some(row)) => (row.did, row.key_bytes), 379 Ok(None) => { 380 return ( 381 StatusCode::UNAUTHORIZED, 382 Json(json!({"error": "AuthenticationFailed"})), 383 ) 384 .into_response(); 385 } 386 Err(e) => { 387 error!("DB error in request_account_delete: {:?}", e); 388 return ( 389 StatusCode::INTERNAL_SERVER_ERROR, 390 Json(json!({"error": "InternalError"})), 391 ) 392 .into_response(); 393 } 394 }; 395 396 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 397 return ( 398 StatusCode::UNAUTHORIZED, 399 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 400 ) 401 .into_response(); 402 } 403 404 let confirmation_token = Uuid::new_v4().to_string(); 405 let expires_at = Utc::now() + Duration::minutes(15); 406 407 let insert = sqlx::query!( 408 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)", 409 confirmation_token, 410 did, 411 expires_at 412 ) 413 .execute(&state.db) 414 .await; 415 416 if let Err(e) = insert { 417 error!("DB error creating deletion token: {:?}", e); 418 return ( 419 StatusCode::INTERNAL_SERVER_ERROR, 420 Json(json!({"error": "InternalError"})), 421 ) 422 .into_response(); 423 } 424 425 // TODO: Send email or other notification 426 info!("Account deletion requested for user {}, token: {}", did, confirmation_token); 427 428 (StatusCode::OK, Json(json!({}))).into_response() 429} 430 431pub async fn refresh_session( 432 State(state): State<AppState>, 433 headers: axum::http::HeaderMap, 434) -> Response { 435 let auth_header = headers.get("Authorization"); 436 if auth_header.is_none() { 437 return ( 438 StatusCode::UNAUTHORIZED, 439 Json(json!({"error": "AuthenticationRequired"})), 440 ) 441 .into_response(); 442 } 443 444 let refresh_token = auth_header 445 .unwrap() 446 .to_str() 447 .unwrap_or("") 448 .replace("Bearer ", ""); 449 450 let session = sqlx::query!( 451 "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1", 452 refresh_token 453 ) 454 .fetch_optional(&state.db) 455 .await; 456 457 match session { 458 Ok(Some(session_row)) => { 459 let did = &session_row.did; 460 let key_bytes = &session_row.key_bytes; 461 462 if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) { 463 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response(); 464 } 465 466 let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) { 467 Ok(t) => t, 468 Err(e) => { 469 error!("Failed to create access token: {:?}", e); 470 return ( 471 StatusCode::INTERNAL_SERVER_ERROR, 472 Json(json!({"error": "InternalError"})), 473 ) 474 .into_response(); 475 } 476 }; 477 let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) { 478 Ok(t) => t, 479 Err(e) => { 480 error!("Failed to create refresh token: {:?}", e); 481 return ( 482 StatusCode::INTERNAL_SERVER_ERROR, 483 Json(json!({"error": "InternalError"})), 484 ) 485 .into_response(); 486 } 487 }; 488 489 let update = sqlx::query!( 490 "UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3", 491 new_access_jwt, 492 new_refresh_jwt, 493 refresh_token 494 ) 495 .execute(&state.db) 496 .await; 497 498 match update { 499 Ok(_) => { 500 let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", did) 501 .fetch_optional(&state.db) 502 .await; 503 504 match user { 505 Ok(Some(u)) => { 506 return ( 507 StatusCode::OK, 508 Json(json!({ 509 "accessJwt": new_access_jwt, 510 "refreshJwt": new_refresh_jwt, 511 "handle": u.handle, 512 "did": did 513 })), 514 ) 515 .into_response(); 516 } 517 Ok(None) => { 518 error!("User not found for existing session: {}", did); 519 return ( 520 StatusCode::INTERNAL_SERVER_ERROR, 521 Json(json!({"error": "InternalError"})), 522 ) 523 .into_response(); 524 } 525 Err(e) => { 526 error!("Database error fetching user: {:?}", e); 527 return ( 528 StatusCode::INTERNAL_SERVER_ERROR, 529 Json(json!({"error": "InternalError"})), 530 ) 531 .into_response(); 532 } 533 } 534 } 535 Err(e) => { 536 error!("Database error updating session: {:?}", e); 537 return ( 538 StatusCode::INTERNAL_SERVER_ERROR, 539 Json(json!({"error": "InternalError"})), 540 ) 541 .into_response(); 542 } 543 } 544 } 545 Ok(None) => { 546 return ( 547 StatusCode::UNAUTHORIZED, 548 Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"})), 549 ) 550 .into_response(); 551 } 552 Err(e) => { 553 error!("Database error fetching session: {:?}", e); 554 return ( 555 StatusCode::INTERNAL_SERVER_ERROR, 556 Json(json!({"error": "InternalError"})), 557 ) 558 .into_response(); 559 } 560 } 561} 562 563#[derive(Serialize)] 564#[serde(rename_all = "camelCase")] 565pub struct CheckAccountStatusOutput { 566 pub activated: bool, 567 pub valid_did: bool, 568 pub repo_commit: String, 569 pub repo_rev: String, 570 pub repo_blocks: i64, 571 pub indexed_records: i64, 572 pub private_state_values: i64, 573 pub expected_blobs: i64, 574 pub imported_blobs: i64, 575} 576 577pub async fn check_account_status( 578 State(state): State<AppState>, 579 headers: axum::http::HeaderMap, 580) -> Response { 581 let auth_header = headers.get("Authorization"); 582 if auth_header.is_none() { 583 return ( 584 StatusCode::UNAUTHORIZED, 585 Json(json!({"error": "AuthenticationRequired"})), 586 ) 587 .into_response(); 588 } 589 590 let token = auth_header 591 .unwrap() 592 .to_str() 593 .unwrap_or("") 594 .replace("Bearer ", ""); 595 596 let session = sqlx::query!( 597 r#" 598 SELECT s.did, k.key_bytes, u.id as user_id 599 FROM sessions s 600 JOIN users u ON s.did = u.did 601 JOIN user_keys k ON u.id = k.user_id 602 WHERE s.access_jwt = $1 603 "#, 604 token 605 ) 606 .fetch_optional(&state.db) 607 .await; 608 609 let (did, key_bytes, user_id) = match session { 610 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 611 Ok(None) => { 612 return ( 613 StatusCode::UNAUTHORIZED, 614 Json(json!({"error": "AuthenticationFailed"})), 615 ) 616 .into_response(); 617 } 618 Err(e) => { 619 error!("DB error in check_account_status: {:?}", e); 620 return ( 621 StatusCode::INTERNAL_SERVER_ERROR, 622 Json(json!({"error": "InternalError"})), 623 ) 624 .into_response(); 625 } 626 }; 627 628 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 629 return ( 630 StatusCode::UNAUTHORIZED, 631 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 632 ) 633 .into_response(); 634 } 635 636 let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did) 637 .fetch_optional(&state.db) 638 .await; 639 640 let deactivated_at = match user_status { 641 Ok(Some(row)) => row.deactivated_at, 642 _ => None, 643 }; 644 645 let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id) 646 .fetch_optional(&state.db) 647 .await; 648 649 let repo_commit = match repo_result { 650 Ok(Some(row)) => row.repo_root_cid, 651 _ => String::new(), 652 }; 653 654 let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id) 655 .fetch_one(&state.db) 656 .await 657 .unwrap_or(Some(0)) 658 .unwrap_or(0); 659 660 let blob_count: i64 = 661 sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id) 662 .fetch_one(&state.db) 663 .await 664 .unwrap_or(Some(0)) 665 .unwrap_or(0); 666 667 let valid_did = did.starts_with("did:"); 668 669 ( 670 StatusCode::OK, 671 Json(CheckAccountStatusOutput { 672 activated: deactivated_at.is_none(), 673 valid_did, 674 repo_commit: repo_commit.clone(), 675 repo_rev: chrono::Utc::now().timestamp_millis().to_string(), 676 repo_blocks: 0, 677 indexed_records: record_count, 678 private_state_values: 0, 679 expected_blobs: blob_count, 680 imported_blobs: blob_count, 681 }), 682 ) 683 .into_response() 684} 685 686pub async fn activate_account( 687 State(state): State<AppState>, 688 headers: axum::http::HeaderMap, 689) -> Response { 690 let auth_header = headers.get("Authorization"); 691 if auth_header.is_none() { 692 return ( 693 StatusCode::UNAUTHORIZED, 694 Json(json!({"error": "AuthenticationRequired"})), 695 ) 696 .into_response(); 697 } 698 699 let token = auth_header 700 .unwrap() 701 .to_str() 702 .unwrap_or("") 703 .replace("Bearer ", ""); 704 705 let session = sqlx::query!( 706 r#" 707 SELECT s.did, k.key_bytes 708 FROM sessions s 709 JOIN users u ON s.did = u.did 710 JOIN user_keys k ON u.id = k.user_id 711 WHERE s.access_jwt = $1 712 "#, 713 token 714 ) 715 .fetch_optional(&state.db) 716 .await; 717 718 let (did, key_bytes) = match session { 719 Ok(Some(row)) => (row.did, row.key_bytes), 720 Ok(None) => { 721 return ( 722 StatusCode::UNAUTHORIZED, 723 Json(json!({"error": "AuthenticationFailed"})), 724 ) 725 .into_response(); 726 } 727 Err(e) => { 728 error!("DB error in activate_account: {:?}", e); 729 return ( 730 StatusCode::INTERNAL_SERVER_ERROR, 731 Json(json!({"error": "InternalError"})), 732 ) 733 .into_response(); 734 } 735 }; 736 737 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 738 return ( 739 StatusCode::UNAUTHORIZED, 740 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 741 ) 742 .into_response(); 743 } 744 745 let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did) 746 .execute(&state.db) 747 .await; 748 749 match result { 750 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 751 Err(e) => { 752 error!("DB error activating account: {:?}", e); 753 ( 754 StatusCode::INTERNAL_SERVER_ERROR, 755 Json(json!({"error": "InternalError"})), 756 ) 757 .into_response() 758 } 759 } 760} 761 762#[derive(Deserialize)] 763#[serde(rename_all = "camelCase")] 764pub struct DeactivateAccountInput { 765 pub delete_after: Option<String>, 766} 767 768pub async fn deactivate_account( 769 State(state): State<AppState>, 770 headers: axum::http::HeaderMap, 771 Json(_input): Json<DeactivateAccountInput>, 772) -> Response { 773 let auth_header = headers.get("Authorization"); 774 if auth_header.is_none() { 775 return ( 776 StatusCode::UNAUTHORIZED, 777 Json(json!({"error": "AuthenticationRequired"})), 778 ) 779 .into_response(); 780 } 781 782 let token = auth_header 783 .unwrap() 784 .to_str() 785 .unwrap_or("") 786 .replace("Bearer ", ""); 787 788 let session = sqlx::query!( 789 r#" 790 SELECT s.did, k.key_bytes 791 FROM sessions s 792 JOIN users u ON s.did = u.did 793 JOIN user_keys k ON u.id = k.user_id 794 WHERE s.access_jwt = $1 795 "#, 796 token 797 ) 798 .fetch_optional(&state.db) 799 .await; 800 801 let (did, key_bytes) = match session { 802 Ok(Some(row)) => (row.did, row.key_bytes), 803 Ok(None) => { 804 return ( 805 StatusCode::UNAUTHORIZED, 806 Json(json!({"error": "AuthenticationFailed"})), 807 ) 808 .into_response(); 809 } 810 Err(e) => { 811 error!("DB error in deactivate_account: {:?}", e); 812 return ( 813 StatusCode::INTERNAL_SERVER_ERROR, 814 Json(json!({"error": "InternalError"})), 815 ) 816 .into_response(); 817 } 818 }; 819 820 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 821 return ( 822 StatusCode::UNAUTHORIZED, 823 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 824 ) 825 .into_response(); 826 } 827 828 let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did) 829 .execute(&state.db) 830 .await; 831 832 match result { 833 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 834 Err(e) => { 835 error!("DB error deactivating account: {:?}", e); 836 ( 837 StatusCode::INTERNAL_SERVER_ERROR, 838 Json(json!({"error": "InternalError"})), 839 ) 840 .into_response() 841 } 842 } 843} 844 845#[derive(Serialize)] 846#[serde(rename_all = "camelCase")] 847pub struct AppPassword { 848 pub name: String, 849 pub created_at: String, 850 pub privileged: bool, 851} 852 853#[derive(Serialize)] 854pub struct ListAppPasswordsOutput { 855 pub passwords: Vec<AppPassword>, 856} 857 858pub async fn list_app_passwords( 859 State(state): State<AppState>, 860 headers: axum::http::HeaderMap, 861) -> Response { 862 let auth_header = headers.get("Authorization"); 863 if auth_header.is_none() { 864 return ( 865 StatusCode::UNAUTHORIZED, 866 Json(json!({"error": "AuthenticationRequired"})), 867 ) 868 .into_response(); 869 } 870 871 let token = auth_header 872 .unwrap() 873 .to_str() 874 .unwrap_or("") 875 .replace("Bearer ", ""); 876 877 let session = sqlx::query!( 878 r#" 879 SELECT s.did, k.key_bytes, u.id as user_id 880 FROM sessions s 881 JOIN users u ON s.did = u.did 882 JOIN user_keys k ON u.id = k.user_id 883 WHERE s.access_jwt = $1 884 "#, 885 token 886 ) 887 .fetch_optional(&state.db) 888 .await; 889 890 let (_did, key_bytes, user_id) = match session { 891 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 892 Ok(None) => { 893 return ( 894 StatusCode::UNAUTHORIZED, 895 Json(json!({"error": "AuthenticationFailed"})), 896 ) 897 .into_response(); 898 } 899 Err(e) => { 900 error!("DB error in list_app_passwords: {:?}", e); 901 return ( 902 StatusCode::INTERNAL_SERVER_ERROR, 903 Json(json!({"error": "InternalError"})), 904 ) 905 .into_response(); 906 } 907 }; 908 909 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 910 return ( 911 StatusCode::UNAUTHORIZED, 912 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 913 ) 914 .into_response(); 915 } 916 917 let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id) 918 .fetch_all(&state.db) 919 .await; 920 921 match result { 922 Ok(rows) => { 923 let passwords: Vec<AppPassword> = rows 924 .iter() 925 .map(|row| { 926 AppPassword { 927 name: row.name.clone(), 928 created_at: row.created_at.to_rfc3339(), 929 privileged: row.privileged, 930 } 931 }) 932 .collect(); 933 934 (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response() 935 } 936 Err(e) => { 937 error!("DB error listing app passwords: {:?}", e); 938 ( 939 StatusCode::INTERNAL_SERVER_ERROR, 940 Json(json!({"error": "InternalError"})), 941 ) 942 .into_response() 943 } 944 } 945} 946 947#[derive(Deserialize)] 948pub struct CreateAppPasswordInput { 949 pub name: String, 950 pub privileged: Option<bool>, 951} 952 953#[derive(Serialize)] 954#[serde(rename_all = "camelCase")] 955pub struct CreateAppPasswordOutput { 956 pub name: String, 957 pub password: String, 958 pub created_at: String, 959 pub privileged: bool, 960} 961 962pub async fn create_app_password( 963 State(state): State<AppState>, 964 headers: axum::http::HeaderMap, 965 Json(input): Json<CreateAppPasswordInput>, 966) -> Response { 967 let auth_header = headers.get("Authorization"); 968 if auth_header.is_none() { 969 return ( 970 StatusCode::UNAUTHORIZED, 971 Json(json!({"error": "AuthenticationRequired"})), 972 ) 973 .into_response(); 974 } 975 976 let token = auth_header 977 .unwrap() 978 .to_str() 979 .unwrap_or("") 980 .replace("Bearer ", ""); 981 982 let session = sqlx::query!( 983 r#" 984 SELECT s.did, k.key_bytes, u.id as user_id 985 FROM sessions s 986 JOIN users u ON s.did = u.did 987 JOIN user_keys k ON u.id = k.user_id 988 WHERE s.access_jwt = $1 989 "#, 990 token 991 ) 992 .fetch_optional(&state.db) 993 .await; 994 995 let (_did, key_bytes, user_id) = match session { 996 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 997 Ok(None) => { 998 return ( 999 StatusCode::UNAUTHORIZED, 1000 Json(json!({"error": "AuthenticationFailed"})), 1001 ) 1002 .into_response(); 1003 } 1004 Err(e) => { 1005 error!("DB error in create_app_password: {:?}", e); 1006 return ( 1007 StatusCode::INTERNAL_SERVER_ERROR, 1008 Json(json!({"error": "InternalError"})), 1009 ) 1010 .into_response(); 1011 } 1012 }; 1013 1014 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 1015 return ( 1016 StatusCode::UNAUTHORIZED, 1017 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 1018 ) 1019 .into_response(); 1020 } 1021 1022 let name = input.name.trim(); 1023 if name.is_empty() { 1024 return ( 1025 StatusCode::BAD_REQUEST, 1026 Json(json!({"error": "InvalidRequest", "message": "name is required"})), 1027 ) 1028 .into_response(); 1029 } 1030 1031 let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 1032 .fetch_optional(&state.db) 1033 .await; 1034 1035 if let Ok(Some(_)) = existing { 1036 return ( 1037 StatusCode::BAD_REQUEST, 1038 Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})), 1039 ) 1040 .into_response(); 1041 } 1042 1043 let password: String = (0..4) 1044 .map(|_| { 1045 use rand::Rng; 1046 let mut rng = rand::thread_rng(); 1047 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 1048 (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>() 1049 }) 1050 .collect::<Vec<String>>() 1051 .join("-"); 1052 1053 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) { 1054 Ok(h) => h, 1055 Err(e) => { 1056 error!("Failed to hash password: {:?}", e); 1057 return ( 1058 StatusCode::INTERNAL_SERVER_ERROR, 1059 Json(json!({"error": "InternalError"})), 1060 ) 1061 .into_response(); 1062 } 1063 }; 1064 1065 let privileged = input.privileged.unwrap_or(false); 1066 let created_at = chrono::Utc::now(); 1067 1068 let result = sqlx::query!( 1069 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)", 1070 user_id, 1071 name, 1072 password_hash, 1073 created_at, 1074 privileged 1075 ) 1076 .execute(&state.db) 1077 .await; 1078 1079 match result { 1080 Ok(_) => ( 1081 StatusCode::OK, 1082 Json(CreateAppPasswordOutput { 1083 name: name.to_string(), 1084 password, 1085 created_at: created_at.to_rfc3339(), 1086 privileged, 1087 }), 1088 ) 1089 .into_response(), 1090 Err(e) => { 1091 error!("DB error creating app password: {:?}", e); 1092 ( 1093 StatusCode::INTERNAL_SERVER_ERROR, 1094 Json(json!({"error": "InternalError"})), 1095 ) 1096 .into_response() 1097 } 1098 } 1099} 1100 1101#[derive(Deserialize)] 1102pub struct RevokeAppPasswordInput { 1103 pub name: String, 1104} 1105 1106pub async fn revoke_app_password( 1107 State(state): State<AppState>, 1108 headers: axum::http::HeaderMap, 1109 Json(input): Json<RevokeAppPasswordInput>, 1110) -> Response { 1111 let auth_header = headers.get("Authorization"); 1112 if auth_header.is_none() { 1113 return ( 1114 StatusCode::UNAUTHORIZED, 1115 Json(json!({"error": "AuthenticationRequired"})), 1116 ) 1117 .into_response(); 1118 } 1119 1120 let token = auth_header 1121 .unwrap() 1122 .to_str() 1123 .unwrap_or("") 1124 .replace("Bearer ", ""); 1125 1126 let session = sqlx::query!( 1127 r#" 1128 SELECT s.did, k.key_bytes, u.id as user_id 1129 FROM sessions s 1130 JOIN users u ON s.did = u.did 1131 JOIN user_keys k ON u.id = k.user_id 1132 WHERE s.access_jwt = $1 1133 "#, 1134 token 1135 ) 1136 .fetch_optional(&state.db) 1137 .await; 1138 1139 let (_did, key_bytes, user_id) = match session { 1140 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 1141 Ok(None) => { 1142 return ( 1143 StatusCode::UNAUTHORIZED, 1144 Json(json!({"error": "AuthenticationFailed"})), 1145 ) 1146 .into_response(); 1147 } 1148 Err(e) => { 1149 error!("DB error in revoke_app_password: {:?}", e); 1150 return ( 1151 StatusCode::INTERNAL_SERVER_ERROR, 1152 Json(json!({"error": "InternalError"})), 1153 ) 1154 .into_response(); 1155 } 1156 }; 1157 1158 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 1159 return ( 1160 StatusCode::UNAUTHORIZED, 1161 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 1162 ) 1163 .into_response(); 1164 } 1165 1166 let name = input.name.trim(); 1167 if name.is_empty() { 1168 return ( 1169 StatusCode::BAD_REQUEST, 1170 Json(json!({"error": "InvalidRequest", "message": "name is required"})), 1171 ) 1172 .into_response(); 1173 } 1174 1175 let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 1176 .execute(&state.db) 1177 .await; 1178 1179 match result { 1180 Ok(r) => { 1181 if r.rows_affected() == 0 { 1182 return ( 1183 StatusCode::NOT_FOUND, 1184 Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})), 1185 ) 1186 .into_response(); 1187 } 1188 (StatusCode::OK, Json(json!({}))).into_response() 1189 } 1190 Err(e) => { 1191 error!("DB error revoking app password: {:?}", e); 1192 ( 1193 StatusCode::INTERNAL_SERVER_ERROR, 1194 Json(json!({"error": "InternalError"})), 1195 ) 1196 .into_response() 1197 } 1198 } 1199}