accounts.rs (23054B)
1 use crate::{ 2 api::{EmptyResult, JsonResult, PasswordOrOtpData}, 3 auth::{decode_delete, ClientHeaders, Headers}, 4 db::{ 5 models::{Cipher, Device, Folder, User, UserKdfType, UserOrganization}, 6 DbConn, 7 }, 8 error::Error, 9 util::NumberOrString, 10 }; 11 use rocket::serde::json::Json; 12 use rocket::{ 13 http::Status, 14 request::{FromRequest, Outcome, Request}, 15 }; 16 use serde_json::Value; 17 18 pub fn routes() -> Vec<rocket::Route> { 19 routes![ 20 api_key, 21 delete_account, 22 get_auth_request, 23 get_auth_request_response, 24 get_auth_requests, 25 get_known_device, 26 get_public_keys, 27 password_hint, 28 post_auth_request, 29 post_clear_device_token, 30 post_delete_account, 31 post_delete_recover, 32 post_delete_recover_token, 33 post_device_token, 34 post_email, 35 post_email_token, 36 post_kdf, 37 post_keys, 38 post_password, 39 post_profile, 40 post_rotatekey, 41 post_sstamp, 42 post_verify_email, 43 post_verify_email_token, 44 prelogin, 45 profile, 46 put_auth_request, 47 put_avatar, 48 put_clear_device_token, 49 put_device_token, 50 put_profile, 51 register, 52 revision_date, 53 rotate_api_key, 54 verify_password, 55 ] 56 } 57 58 #[derive(Debug, Deserialize)] 59 #[serde(rename_all = "camelCase")] 60 pub struct RegisterData { 61 #[allow(dead_code)] 62 email: String, 63 #[allow(dead_code)] 64 kdf: Option<i32>, 65 #[allow(dead_code)] 66 kdf_iterations: Option<i32>, 67 #[allow(dead_code)] 68 kdf_memory: Option<i32>, 69 #[allow(dead_code)] 70 kdf_parallelism: Option<i32>, 71 #[allow(dead_code)] 72 key: String, 73 #[allow(dead_code)] 74 keys: Option<KeysData>, 75 #[allow(dead_code)] 76 master_password_hash: String, 77 #[allow(dead_code)] 78 master_password_hint: Option<String>, 79 #[allow(dead_code)] 80 name: Option<String>, 81 #[allow(dead_code)] 82 token: Option<String>, 83 #[allow(dead_code)] 84 organization_user_id: Option<String>, 85 } 86 87 #[derive(Debug, Deserialize)] 88 #[serde(rename_all = "camelCase")] 89 struct KeysData { 90 encrypted_private_key: String, 91 public_key: String, 92 } 93 94 /// Trims whitespace from password hints, and converts blank password hints to `None`. 95 fn clean_password_hint(password_hint: &Option<String>) -> Option<String> { 96 password_hint.as_ref().and_then(|h| match h.trim() { 97 "" => None, 98 ht => Some(ht.to_owned()), 99 }) 100 } 101 102 fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult { 103 if password_hint.is_some() { 104 err!("Password hints have been disabled by the administrator. Remove the hint and try again."); 105 } 106 Ok(()) 107 } 108 109 #[allow(unused_variables, clippy::needless_pass_by_value)] 110 #[post("/accounts/register", data = "<data>")] 111 fn register(data: Json<RegisterData>) -> Error { 112 const MSG: &str = "Registration is permanently disabled."; 113 Error::new(MSG, MSG) 114 } 115 #[get("/accounts/profile")] 116 async fn profile(headers: Headers, conn: DbConn) -> Json<Value> { 117 Json(headers.user.to_json(&conn).await) 118 } 119 120 #[derive(Deserialize)] 121 #[serde(rename_all = "camelCase")] 122 struct ProfileData { 123 // Culture: String, // Ignored, always use en-US 124 // MasterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData 125 name: String, 126 } 127 128 #[put("/accounts/profile", data = "<data>")] 129 async fn put_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { 130 post_profile(data, headers, conn).await 131 } 132 133 #[post("/accounts/profile", data = "<data>")] 134 async fn post_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { 135 let prof_data: ProfileData = data.into_inner(); 136 // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden) 137 // This also prevents issues with very long usernames causing to large JWT's. See #2419 138 if prof_data.name.len() > 50 { 139 err!("The field Name must be a string with a maximum length of 50."); 140 } 141 let mut user = headers.user; 142 user.name = prof_data.name; 143 user.save(&conn).await?; 144 Ok(Json(user.to_json(&conn).await)) 145 } 146 147 #[derive(Deserialize)] 148 #[serde(rename_all = "camelCase")] 149 struct AvatarData { 150 avatar_color: Option<String>, 151 } 152 153 #[put("/accounts/avatar", data = "<data>")] 154 async fn put_avatar(data: Json<AvatarData>, headers: Headers, conn: DbConn) -> JsonResult { 155 let av_data: AvatarData = data.into_inner(); 156 // It looks like it only supports the 6 hex color format. 157 // If you try to add the short value it will not show that color. 158 // Check and force 7 chars, including the #. 159 if let Some(ref color) = av_data.avatar_color { 160 if color.len() != 7 { 161 err!( 162 "The field AvatarColor must be a HTML/Hex color code with a length of 7 characters" 163 ) 164 } 165 } 166 let mut user = headers.user; 167 user.avatar_color = av_data.avatar_color; 168 user.save(&conn).await?; 169 Ok(Json(user.to_json(&conn).await)) 170 } 171 172 #[get("/users/<uuid>/public-key")] 173 async fn get_public_keys(uuid: &str, _headers: Headers, conn: DbConn) -> JsonResult { 174 let Some(user) = User::find_by_uuid(uuid, &conn).await else { 175 err!("User doesn't exist") 176 }; 177 Ok(Json(json!({ 178 "userId": user.uuid, 179 "publicKey": user.public_key, 180 "object":"userKey" 181 }))) 182 } 183 184 #[post("/accounts/keys", data = "<data>")] 185 async fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult { 186 let key_data: KeysData = data.into_inner(); 187 let mut user = headers.user; 188 user.private_key = Some(key_data.encrypted_private_key); 189 user.public_key = Some(key_data.public_key); 190 user.save(&conn).await?; 191 Ok(Json(json!({ 192 "privateKey": user.private_key, 193 "publicKey": user.public_key, 194 "object":"keys" 195 }))) 196 } 197 198 #[derive(Deserialize)] 199 #[serde(rename_all = "camelCase")] 200 struct ChangePassData { 201 master_password_hash: String, 202 new_master_password_hash: String, 203 master_password_hint: Option<String>, 204 key: String, 205 } 206 207 #[post("/accounts/password", data = "<data>")] 208 async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbConn) -> EmptyResult { 209 let pass_data: ChangePassData = data.into_inner(); 210 let mut user = headers.user; 211 if !user.check_valid_password(&pass_data.master_password_hash) { 212 err!("Invalid password") 213 } 214 user.password_hint = clean_password_hint(&pass_data.master_password_hint); 215 enforce_password_hint_setting(&user.password_hint)?; 216 user.set_password( 217 &pass_data.new_master_password_hash, 218 Some(pass_data.key), 219 true, 220 Some(vec![ 221 String::from("post_rotatekey"), 222 String::from("get_contacts"), 223 String::from("get_public_keys"), 224 ]), 225 ); 226 user.save(&conn).await 227 } 228 229 #[derive(Deserialize)] 230 #[serde(rename_all = "camelCase")] 231 struct ChangeKdfData { 232 kdf: i32, 233 kdf_iterations: u32, 234 kdf_memory: Option<u32>, 235 kdf_parallelism: Option<u32>, 236 master_password_hash: String, 237 new_master_password_hash: String, 238 key: String, 239 } 240 241 #[post("/accounts/kdf", data = "<data>")] 242 async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult { 243 let kdf_data: ChangeKdfData = data.into_inner(); 244 let mut user = headers.user; 245 if !user.check_valid_password(&kdf_data.master_password_hash) { 246 err!("Invalid password") 247 } 248 if kdf_data.kdf == i32::from(UserKdfType::Pbkdf2) && kdf_data.kdf_iterations < 100_000u32 { 249 err!("PBKDF2 KDF iterations must be at least 100000.") 250 } 251 if kdf_data.kdf == i32::from(UserKdfType::Argon2id) { 252 if kdf_data.kdf_iterations < 1u32 { 253 err!("Argon2 KDF iterations must be at least 1.") 254 } 255 if let Some(m) = kdf_data.kdf_memory { 256 if !(15u32..=1024u32).contains(&m) { 257 err!("Argon2 memory must be between 15 MB and 1024 MB.") 258 } 259 user.set_client_kdf_memory(kdf_data.kdf_memory); 260 } else { 261 err!("Argon2 memory parameter is required.") 262 } 263 if let Some(p) = kdf_data.kdf_parallelism { 264 if !(1u32..=16u32).contains(&p) { 265 err!("Argon2 parallelism must be between 1 and 16.") 266 } 267 user.set_client_kdf_parallelism(kdf_data.kdf_parallelism); 268 } else { 269 err!("Argon2 parallelism parameter is required.") 270 } 271 } else { 272 user.set_client_kdf_memory(None); 273 user.set_client_kdf_parallelism(None); 274 } 275 user.set_client_kdf_iter(kdf_data.kdf_iterations); 276 user.client_kdf_type = kdf_data.kdf; 277 user.set_password( 278 &kdf_data.new_master_password_hash, 279 Some(kdf_data.key), 280 true, 281 None, 282 ); 283 user.save(&conn).await 284 } 285 286 #[derive(Deserialize)] 287 #[serde(rename_all = "camelCase")] 288 struct UpdateFolderData { 289 id: Option<String>, 290 name: String, 291 } 292 293 #[derive(Deserialize)] 294 #[serde(rename_all = "camelCase")] 295 struct UpdateResetPasswordData { 296 organization_id: String, 297 reset_password_key: String, 298 } 299 300 use super::ciphers::CipherData; 301 302 #[derive(Deserialize)] 303 #[serde(rename_all = "camelCase")] 304 struct KeyData { 305 ciphers: Vec<CipherData>, 306 folders: Vec<UpdateFolderData>, 307 reset_password_keys: Vec<UpdateResetPasswordData>, 308 key: String, 309 master_password_hash: String, 310 private_key: String, 311 } 312 313 #[post("/accounts/key", data = "<data>")] 314 async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn) -> EmptyResult { 315 let key_data: KeyData = data.into_inner(); 316 if !headers 317 .user 318 .check_valid_password(&key_data.master_password_hash) 319 { 320 err!("Invalid password") 321 } 322 // Validate the import before continuing 323 // Bitwarden does not process the import if there is one item invalid. 324 // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. 325 // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. 326 Cipher::validate_cipher_data(&key_data.ciphers)?; 327 let user_uuid = &headers.user.uuid; 328 // Update folder data 329 for folder_data in key_data.folders { 330 if let Some(folder_id) = folder_data.id { 331 let Some(mut saved_folder) = Folder::find_by_uuid(&folder_id, &conn).await else { 332 err!("Folder doesn't exist") 333 }; 334 if &saved_folder.user_uuid != user_uuid { 335 err!("The folder is not owned by the user") 336 } 337 338 saved_folder.name = folder_data.name; 339 saved_folder.save(&conn).await?; 340 } 341 } 342 for reset_password_data in key_data.reset_password_keys { 343 let Some(mut user_org) = UserOrganization::find_by_user_and_org( 344 user_uuid, 345 &reset_password_data.organization_id, 346 &conn, 347 ) 348 .await 349 else { 350 err!("Reset password doesn't exist") 351 }; 352 user_org.reset_password_key = Some(reset_password_data.reset_password_key); 353 user_org.save(&conn).await?; 354 } 355 // Update cipher data 356 use super::ciphers::update_cipher_from_data; 357 for cipher_data in key_data.ciphers { 358 if cipher_data.organization_id.is_none() { 359 let Some(mut saved_cipher) = 360 Cipher::find_by_uuid(cipher_data.id.as_ref().unwrap(), &conn).await 361 else { 362 err!("Cipher doesn't exist") 363 }; 364 if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid { 365 err!("The cipher is not owned by the user") 366 } 367 update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, None, &conn, true) 368 .await?; 369 } 370 } 371 372 // Update user data 373 let mut user = headers.user; 374 user.akey = key_data.key; 375 user.private_key = Some(key_data.private_key); 376 user.reset_security_stamp(); 377 user.save(&conn).await 378 } 379 380 #[post("/accounts/security-stamp", data = "<data>")] 381 async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult { 382 let otp_data: PasswordOrOtpData = data.into_inner(); 383 let mut user = headers.user; 384 otp_data.validate(&user)?; 385 Device::delete_all_by_user(&user.uuid, &conn).await?; 386 user.reset_security_stamp(); 387 user.save(&conn).await 388 } 389 390 #[derive(Deserialize)] 391 #[serde(rename_all = "camelCase")] 392 struct EmailTokenData { 393 #[allow(dead_code)] 394 master_password_hash: String, 395 #[allow(dead_code)] 396 new_email: String, 397 } 398 399 #[allow(unused_variables, clippy::needless_pass_by_value)] 400 #[post("/accounts/email-token", data = "<data>")] 401 fn post_email_token(data: Json<EmailTokenData>, _headers: Headers) -> Error { 402 const MSG: &str = "E-mail change is not allowed."; 403 Error::new(MSG, MSG) 404 } 405 406 #[derive(Deserialize)] 407 #[serde(rename_all = "camelCase")] 408 struct ChangeEmailData { 409 #[allow(dead_code)] 410 master_password_hash: String, 411 #[allow(dead_code)] 412 new_email: String, 413 #[allow(dead_code)] 414 key: String, 415 #[allow(dead_code)] 416 new_master_password_hash: String, 417 #[allow(dead_code)] 418 token: NumberOrString, 419 } 420 421 #[allow(unused_variables, clippy::needless_pass_by_value)] 422 #[post("/accounts/email", data = "<data>")] 423 fn post_email(data: Json<ChangeEmailData>, _headers: Headers) -> Error { 424 const MSG: &str = "E-mail change is not allowed."; 425 Error::new(MSG, MSG) 426 } 427 428 #[allow(clippy::needless_pass_by_value)] 429 #[post("/accounts/verify-email")] 430 fn post_verify_email(_headers: Headers) -> Error { 431 const MSG: &str = "E-mail is disabled."; 432 Error::new(MSG, MSG) 433 } 434 435 #[derive(Deserialize)] 436 #[serde(rename_all = "camelCase")] 437 struct VerifyEmailTokenData { 438 #[allow(dead_code)] 439 user_id: String, 440 #[allow(dead_code)] 441 token: String, 442 } 443 444 #[allow(unused_variables, clippy::needless_pass_by_value)] 445 #[post("/accounts/verify-email-token", data = "<data>")] 446 fn post_verify_email_token(data: Json<VerifyEmailTokenData>) -> Error { 447 const MSG: &str = "E-mail is disabled."; 448 Error::new(MSG, MSG) 449 } 450 451 #[derive(Deserialize)] 452 #[serde(rename_all = "camelCase")] 453 struct DeleteRecoverData { 454 #[allow(dead_code)] 455 email: String, 456 } 457 458 #[allow(unused_variables, clippy::needless_pass_by_value)] 459 #[post("/accounts/delete-recover", data = "<data>")] 460 fn post_delete_recover(data: Json<DeleteRecoverData>) -> Error { 461 const MSG: &str = "Account deletion is disabled with at this endpoint."; 462 Error::new(MSG, MSG) 463 } 464 465 #[derive(Deserialize)] 466 #[serde(rename_all = "camelCase")] 467 struct DeleteRecoverTokenData { 468 user_id: String, 469 token: String, 470 } 471 472 #[post("/accounts/delete-recover-token", data = "<data>")] 473 async fn post_delete_recover_token( 474 data: Json<DeleteRecoverTokenData>, 475 conn: DbConn, 476 ) -> EmptyResult { 477 let token_data: DeleteRecoverTokenData = data.into_inner(); 478 let Some(user) = User::find_by_uuid(&token_data.user_id, &conn).await else { 479 err!("User doesn't exist") 480 }; 481 let Ok(claims) = decode_delete(&token_data.token) else { 482 err!("Invalid claim") 483 }; 484 if claims.sub != user.uuid { 485 err!("Invalid claim"); 486 } 487 user.delete(&conn).await 488 } 489 490 #[post("/accounts/delete", data = "<data>")] 491 async fn post_delete_account( 492 data: Json<PasswordOrOtpData>, 493 headers: Headers, 494 conn: DbConn, 495 ) -> EmptyResult { 496 delete_account(data, headers, conn).await 497 } 498 499 #[delete("/accounts", data = "<data>")] 500 async fn delete_account( 501 data: Json<PasswordOrOtpData>, 502 headers: Headers, 503 conn: DbConn, 504 ) -> EmptyResult { 505 let otp_data: PasswordOrOtpData = data.into_inner(); 506 let user = headers.user; 507 otp_data.validate(&user)?; 508 user.delete(&conn).await 509 } 510 #[allow(clippy::needless_pass_by_value)] 511 #[get("/accounts/revision-date")] 512 fn revision_date(headers: Headers) -> Json<Value> { 513 Json(json!(headers.user.updated_at.and_utc().timestamp_millis())) 514 } 515 516 #[derive(Deserialize)] 517 #[serde(rename_all = "camelCase")] 518 struct PasswordHintData { 519 #[allow(dead_code)] 520 email: String, 521 } 522 523 #[allow(unused_variables, clippy::needless_pass_by_value)] 524 #[post("/accounts/password-hint", data = "<data>")] 525 fn password_hint(data: Json<PasswordHintData>) -> Error { 526 const MSG: &str = "Password hints are disabled."; 527 Error::new(MSG, MSG) 528 } 529 530 #[derive(Deserialize)] 531 #[serde(rename_all = "camelCase")] 532 pub struct PreloginData { 533 email: String, 534 } 535 536 #[post("/accounts/prelogin", data = "<data>")] 537 async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> { 538 _prelogin(data, conn).await 539 } 540 541 pub async fn _prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> { 542 let login_data: PreloginData = data.into_inner(); 543 let (kdf_type, kdf_iter, kdf_mem, kdf_para) = 544 match User::find_by_mail(&login_data.email, &conn).await { 545 Some(user) => ( 546 user.client_kdf_type, 547 user.client_kdf_iter(), 548 user.client_kdf_memory(), 549 user.client_kdf_parallelism(), 550 ), 551 None => ( 552 User::client_kdf_type_default(), 553 User::CLIENT_KDF_ITER_DEFAULT, 554 None, 555 None, 556 ), 557 }; 558 let result = json!({ 559 "kdf": kdf_type, 560 "kdfIterations": kdf_iter, 561 "kdfMemory": kdf_mem, 562 "kdfParallelism": kdf_para, 563 }); 564 Json(result) 565 } 566 567 // https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs 568 #[derive(Deserialize)] 569 #[serde(rename_all = "camelCase")] 570 struct SecretVerificationRequest { 571 master_password_hash: String, 572 } 573 574 #[post("/accounts/verify-password", data = "<data>")] 575 fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers) -> EmptyResult { 576 let req: SecretVerificationRequest = data.into_inner(); 577 let user = headers.user; 578 if !user.check_valid_password(&req.master_password_hash) { 579 err!("Invalid password") 580 } 581 Ok(()) 582 } 583 584 const API_DISABLED_MSG: &str = "API access is disabled."; 585 #[allow(unused_variables, clippy::needless_pass_by_value)] 586 #[post("/accounts/api-key", data = "<data>")] 587 fn api_key(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { 588 Error::new(API_DISABLED_MSG, API_DISABLED_MSG) 589 } 590 591 #[allow(unused_variables, clippy::needless_pass_by_value)] 592 #[post("/accounts/rotate-api-key", data = "<data>")] 593 fn rotate_api_key(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { 594 Error::new(API_DISABLED_MSG, API_DISABLED_MSG) 595 } 596 597 #[get("/devices/knowndevice")] 598 async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult { 599 let mut result = false; 600 if let Some(user) = User::find_by_mail(&device.email, &conn).await { 601 result = Device::find_by_uuid_and_user(&device.uuid, &user.uuid, &conn) 602 .await 603 .is_some(); 604 } 605 Ok(Json(json!(result))) 606 } 607 608 struct KnownDevice { 609 email: String, 610 uuid: String, 611 } 612 613 #[rocket::async_trait] 614 impl<'r> FromRequest<'r> for KnownDevice { 615 type Error = &'static str; 616 async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { 617 let email = if let Some(email_b64) = request.headers().get_one("X-Request-Email") { 618 let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) 619 else { 620 return Outcome::Error(( 621 Status::BadRequest, 622 "X-Request-Email value failed to decode as base64url", 623 )); 624 }; 625 match String::from_utf8(email_bytes) { 626 Ok(email) => email, 627 Err(_) => { 628 return Outcome::Error(( 629 Status::BadRequest, 630 "X-Request-Email value failed to decode as UTF-8", 631 )) 632 } 633 } 634 } else { 635 return Outcome::Error((Status::BadRequest, "X-Request-Email value is required")); 636 }; 637 638 let uuid = if let Some(uuid) = request.headers().get_one("X-Device-Identifier") { 639 uuid.to_owned() 640 } else { 641 return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required")); 642 }; 643 Outcome::Success(Self { email, uuid }) 644 } 645 } 646 647 #[derive(Deserialize)] 648 #[serde(rename_all = "camelCase")] 649 struct PushToken { 650 #[allow(dead_code)] 651 push_token: String, 652 } 653 654 #[allow(unused_variables, clippy::needless_pass_by_value)] 655 #[post("/devices/identifier/<uuid>/token", data = "<data>")] 656 fn post_device_token(uuid: &str, data: Json<PushToken>, _headers: Headers) {} 657 #[allow(unused_variables, clippy::needless_pass_by_value)] 658 #[put("/devices/identifier/<uuid>/token", data = "<data>")] 659 fn put_device_token(uuid: &str, data: Json<PushToken>, _headers: Headers) {} 660 #[allow(unused_variables)] 661 #[put("/devices/identifier/<uuid>/clear-token")] 662 const fn put_clear_device_token(uuid: &str) {} 663 664 // On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere 665 #[allow(unused_variables)] 666 #[post("/devices/identifier/<uuid>/clear-token")] 667 const fn post_clear_device_token(uuid: &str) {} 668 669 #[derive(Deserialize)] 670 #[serde(rename_all = "camelCase")] 671 struct AuthRequestRequest { 672 #[allow(dead_code)] 673 access_code: String, 674 #[allow(dead_code)] 675 device_identifier: String, 676 #[allow(dead_code)] 677 email: String, 678 #[allow(dead_code)] 679 public_key: String, 680 #[serde(alias = "type")] 681 _type: i32, 682 } 683 const AUTH_DISABLED_MSG: &str = "Auth requests and log in via device are disabled."; 684 #[allow(unused_variables, clippy::needless_pass_by_value)] 685 #[post("/auth-requests", data = "<data>")] 686 fn post_auth_request(data: Json<AuthRequestRequest>, _headers: ClientHeaders) -> Error { 687 Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG) 688 } 689 690 #[allow(unused_variables)] 691 #[get("/auth-requests/<uuid>")] 692 fn get_auth_request(uuid: &str) -> Error { 693 Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG) 694 } 695 696 #[derive(Deserialize)] 697 #[serde(rename_all = "camelCase")] 698 struct AuthResponseRequest { 699 #[allow(dead_code)] 700 device_identifier: String, 701 #[allow(dead_code)] 702 key: String, 703 #[allow(dead_code)] 704 master_password_hash: Option<String>, 705 #[allow(dead_code)] 706 request_approved: bool, 707 } 708 709 #[allow(unused_variables, clippy::needless_pass_by_value)] 710 #[put("/auth-requests/<uuid>", data = "<data>")] 711 fn put_auth_request(uuid: &str, data: Json<AuthResponseRequest>) -> Error { 712 Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG) 713 } 714 715 #[allow(unused_variables)] 716 #[get("/auth-requests/<uuid>/response?<code>")] 717 fn get_auth_request_response(uuid: &str, code: &str) -> Error { 718 Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG) 719 } 720 721 #[allow(unused_variables, clippy::needless_pass_by_value)] 722 #[get("/auth-requests")] 723 fn get_auth_requests(headers: Headers) -> Json<Value> { 724 Json(json!({ 725 "data": Vec::<Value>::new(), 726 "continuationToken": null, 727 "object": "list" 728 })) 729 }