1use bitwarden_core::{
15 UserId,
16 client::persisted_state::{USER_EMAIL, USER_ID},
17};
18use chrono::SecondsFormat;
19use clap::Args;
20use serde::Serialize;
21
22use crate::{
23 client_state::{AnyState, BwCommand},
24 platform::config::resolve_server_url,
25 render::{CommandOutput, CommandResult},
26};
27
28#[derive(Args, Clone)]
29#[command(
30 about = "Show server, last sync, user information, and vault status.",
31 after_help = r#"Example return value:
32 {
33 "serverUrl": "https://bitwarden.example.com",
34 "lastSync": "2020-06-16T06:33:51.419Z",
35 "userEmail": "[email protected]",
36 "userId": "00000000-0000-0000-0000-000000000000",
37 "status": "locked"
38 }
39
40Notes:
41 `status` is one of:
42 - `unauthenticated` when you are not logged in
43 - `locked` when you are logged in and the vault is locked
44 - `unlocked` when you are logged in and the vault is unlocked
45"#
46)]
47pub struct StatusArgs;
48
49#[derive(Serialize)]
52#[serde(rename_all = "camelCase")]
53struct StatusResponse {
54 server_url: String,
55 last_sync: Option<String>,
57 user_email: Option<String>,
58 user_id: Option<UserId>,
59 status: &'static str,
60}
61
62impl BwCommand for StatusArgs {
63 type Client = AnyState;
64
65 async fn run(self, state: AnyState) -> CommandResult {
66 let response = build_status(resolve_server_url()?, state.user).await?;
67 Ok(CommandOutput::Object(Box::new(response)))
68 }
69}
70
71async fn build_status(
74 server_url: String,
75 user: Option<bitwarden_pm::PasswordManagerClient>,
76) -> color_eyre::eyre::Result<StatusResponse> {
77 let Some(user) = user else {
78 return Ok(StatusResponse {
79 server_url,
80 last_sync: None,
81 user_email: None,
82 user_id: None,
83 status: "unauthenticated",
84 });
85 };
86
87 let status = if user.is_unlocked() {
88 "unlocked"
89 } else {
90 "locked"
91 };
92
93 let last_sync = user
94 .sync()
95 .last_sync()
96 .await
97 .map(|t| t.to_rfc3339_opts(SecondsFormat::Millis, true));
98
99 let persisted = user.platform().state();
102
103 Ok(StatusResponse {
104 server_url,
105 last_sync,
106 user_email: persisted.setting(USER_EMAIL)?.get().await?,
107 user_id: persisted.setting(USER_ID)?.get().await?,
108 status,
109 })
110}
111
112#[cfg(test)]
113mod tests {
114 use std::sync::Once;
115
116 use bitwarden_core::{DeviceType, HostPlatformInfo, init_host_platform_info};
117 use bitwarden_pm::PasswordManagerClient;
118
119 use super::*;
120
121 static INIT: Once = Once::new();
124
125 fn ensure_platform_info() {
126 INIT.call_once(|| {
127 init_host_platform_info(HostPlatformInfo {
128 user_agent: "bw-tests".to_string(),
129 device_type: DeviceType::SDK,
130 device_identifier: None,
131 bitwarden_client_version: None,
132 bitwarden_package_type: None,
133 });
134 });
135 }
136
137 async fn status_json(user: Option<PasswordManagerClient>) -> serde_json::Value {
138 ensure_platform_info();
139 let response = build_status("https://example.com".to_string(), user)
140 .await
141 .unwrap();
142 serde_json::to_value(&response).unwrap()
143 }
144
145 #[tokio::test]
146 async fn logged_out_reports_unauthenticated_with_null_user_fields() {
147 let json = status_json(None).await;
148
149 assert_eq!(json["status"], "unauthenticated");
150 assert!(json["lastSync"].is_null());
151 assert!(json["userEmail"].is_null());
152 assert!(json["userId"].is_null());
153 }
154
155 #[tokio::test]
156 async fn field_names_match_the_node_cli() {
157 let json = status_json(None).await;
158
159 let keys: Vec<&str> = json
160 .as_object()
161 .unwrap()
162 .keys()
163 .map(String::as_str)
164 .collect();
165 assert_eq!(
166 keys,
167 ["serverUrl", "lastSync", "userEmail", "userId", "status"],
168 );
169 }
170
171 #[tokio::test]
172 async fn user_without_a_loaded_key_reports_locked() {
173 let json = status_json(Some(PasswordManagerClient::new(None))).await;
175
176 assert_eq!(json["status"], "locked");
177 }
178}