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