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