diff --git a/docs/CommandLineHelp.md b/docs/CommandLineHelp.md index 13e9d01..4af9663 100644 --- a/docs/CommandLineHelp.md +++ b/docs/CommandLineHelp.md @@ -7,6 +7,7 @@ This document contains the help content for the `screenly` command-line program. * [`screenly`↴](#screenly) * [`screenly login`↴](#screenly-login) * [`screenly logout`↴](#screenly-logout) +* [`screenly whoami`↴](#screenly-whoami) * [`screenly screen`↴](#screenly-screen) * [`screenly screen list`↴](#screenly-screen-list) * [`screenly screen get`↴](#screenly-screen-get) @@ -58,6 +59,7 @@ Command line interface is intended for quick interaction with Screenly through t * `login` — Logs in with the provided token and stores it for further use if valid. You can set the API_TOKEN environment variable to override the stored token * `logout` — Logs out and removes the stored token +* `whoami` — Shows information about the currently authenticated user and workspace * `screen` — Screen related commands * `asset` — Asset related commands * `playlist` — Playlist related commands @@ -97,6 +99,14 @@ Logs out and removes the stored token +## `screenly whoami` + +Shows information about the currently authenticated user and workspace + +**Usage:** `screenly whoami` + + + ## `screenly screen` Screen related commands diff --git a/src/cli.rs b/src/cli.rs index 7b283a1..1d485ed 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -94,6 +94,8 @@ pub enum Commands { Login {}, /// Logs out and removes the stored token. Logout {}, + /// Shows information about the currently authenticated user and workspace. + Whoami {}, /// Screen related commands. #[command(subcommand)] Screen(ScreenCommands), @@ -581,6 +583,11 @@ pub fn handle_cli(cli: &Cli) { info!("Logout successful."); std::process::exit(0); } + Commands::Whoami {} => { + let authentication = get_authentication(); + let whoami_command = commands::whoami::WhoamiCommand::new(authentication); + handle_command_execution_result(whoami_command.get(), output); + } Commands::Mcp {} => { handle_cli_mcp_command(); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4856598..9596f7c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -18,6 +18,7 @@ mod ignorer; pub(crate) mod playlist; pub mod screen; pub(crate) mod serde_utils; +pub mod whoami; pub enum OutputType { HumanReadable, @@ -185,7 +186,7 @@ pub fn get( debug!("GET {url} -> {status}"); if status != StatusCode::OK { - println!("Response: {:?}", &response.text()); + debug!("Response: {:?}", &response.text()?); return Err(CommandError::WrongResponseStatus(status.as_u16())); } Ok(serde_json::from_str(&response.text()?)?) diff --git a/src/commands/whoami.rs b/src/commands/whoami.rs new file mode 100644 index 0000000..e867c77 --- /dev/null +++ b/src/commands/whoami.rs @@ -0,0 +1,226 @@ +use prettytable::{Cell, Row}; + +use crate::authentication::Authentication; +use crate::commands::{self, CommandError, Formatter, OutputType}; + +pub struct WhoamiCommand { + authentication: Authentication, +} + +#[derive(Debug)] +pub struct WhoamiInfo { + pub value: serde_json::Value, +} + +impl WhoamiInfo { + pub fn new(value: serde_json::Value) -> Self { + Self { value } + } + + fn field(&self, object: &str, key: &str) -> Option<&str> { + self.value[object][key].as_str().filter(|s| !s.is_empty()) + } + + fn full_name(&self) -> Option { + let parts: Vec<&str> = [ + self.field("user", "first_name"), + self.field("user", "last_name"), + ] + .into_iter() + .flatten() + .collect(); + (!parts.is_empty()).then(|| parts.join(" ")) + } + + /// Shared field list for table and CSV: (table label, csv header, value). + fn rows(&self) -> Vec<(&str, &str, Option)> { + vec![ + ( + "Email", + "email", + self.field("user", "email").map(str::to_string), + ), + ("Name", "name", self.full_name()), + ( + "User ID", + "user_id", + self.field("user", "id").map(str::to_string), + ), + ( + "Workspace", + "workspace", + self.field("workspace", "name").map(str::to_string), + ), + ( + "Workspace ID", + "workspace_id", + self.field("workspace", "id").map(str::to_string), + ), + ( + "Workspace URL", + "workspace_url", + self.field("workspace", "url").map(str::to_string), + ), + ] + } +} + +impl WhoamiCommand { + pub fn new(authentication: Authentication) -> Self { + Self { authentication } + } + + pub fn get(&self) -> Result { + Ok(WhoamiInfo::new(commands::get( + &self.authentication, + "v3/me", + )?)) + } +} + +impl Formatter for WhoamiInfo { + fn supports_csv() -> bool { + true + } + + fn format(&self, output_type: OutputType) -> String { + match output_type { + OutputType::Json => serde_json::to_string_pretty(&self.value).unwrap(), + OutputType::HumanReadable => { + let rows = self.rows(); + let mut table = prettytable::Table::new(); + table.add_row(Row::from(vec!["Field", "Value"])); + for (label, _, value) in &rows { + table.add_row(Row::new(vec![ + Cell::new(label), + Cell::new(value.as_deref().unwrap_or("N/A")), + ])); + } + table.to_string() + } + OutputType::Csv => { + let rows = self.rows(); + let mut wtr = csv::WriterBuilder::new().from_writer(vec![]); + wtr.write_record(rows.iter().map(|(_, header, _)| *header)) + .unwrap(); + wtr.write_record( + rows.iter() + .map(|(_, _, value)| value.as_deref().unwrap_or("")), + ) + .unwrap(); + String::from_utf8(wtr.into_inner().unwrap()).unwrap() + } + } + } +} + +#[cfg(test)] +mod tests { + use httpmock::Method::GET; + use httpmock::MockServer; + use serde_json::json; + + use super::*; + use crate::authentication::{Authentication, Config}; + + fn sample_me() -> serde_json::Value { + json!({ + "workspace": { + "id": "01WORKSPACEID0000000000000", + "name": "Example Workspace", + "url": "https://example.screenlyapp.com" + }, + "user": { + "id": "01USERID000000000000000000", + "first_name": "Ada", + "last_name": "Lovelace", + "email": "ada@example.com" + } + }) + } + + #[test] + fn test_whoami_get_returns_profile() { + let mock_server = MockServer::start(); + let body = sample_me(); + mock_server.mock(|when, then| { + when.method(GET) + .path("/v3/me") + .header("Authorization", "Token token") + .header( + "user-agent", + format!("screenly-cli {}", env!("CARGO_PKG_VERSION")), + ); + then.status(200).json_body(body.clone()); + }); + + let config = Config::new(mock_server.base_url()); + let authentication = Authentication::new_with_config(config, "token"); + let command = WhoamiCommand::new(authentication); + let info = command.get().unwrap(); + assert_eq!(info.value, body); + } + + #[test] + fn test_whoami_human_readable_format() { + let info = WhoamiInfo::new(sample_me()); + let output = info.format(OutputType::HumanReadable); + assert!(output.contains("Email")); + assert!(output.contains("ada@example.com")); + assert!(output.contains("Ada Lovelace")); + assert!(output.contains("Example Workspace")); + assert!(output.contains("https://example.screenlyapp.com")); + } + + #[test] + fn test_whoami_json_format() { + let info = WhoamiInfo::new(sample_me()); + let output = info.format(OutputType::Json); + let parsed: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert_eq!(parsed["user"]["email"], "ada@example.com"); + assert_eq!(parsed["workspace"]["name"], "Example Workspace"); + } + + #[test] + fn test_whoami_csv_format() { + let info = WhoamiInfo::new(sample_me()); + let output = info.format(OutputType::Csv); + let mut lines = output.lines(); + assert_eq!( + lines.next().unwrap(), + "email,name,user_id,workspace,workspace_id,workspace_url" + ); + assert_eq!( + lines.next().unwrap(), + "ada@example.com,Ada Lovelace,01USERID000000000000000000,Example Workspace,01WORKSPACEID0000000000000,https://example.screenlyapp.com" + ); + } + + #[test] + fn test_whoami_missing_and_empty_fields() { + let info = WhoamiInfo::new(json!({ + "workspace": { + "id": "01WORKSPACEID0000000000000", + "name": "Example Workspace" + }, + "user": { + "id": "01USERID000000000000000000", + "first_name": "", + "last_name": "Lovelace", + "email": "ada@example.com" + } + })); + + let table = info.format(OutputType::HumanReadable); + assert!(table.contains("Lovelace")); + assert!(table.contains("N/A")); // missing workspace.url + + let csv = info.format(OutputType::Csv); + let mut lines = csv.lines(); + lines.next(); + assert_eq!( + lines.next().unwrap(), + "ada@example.com,Lovelace,01USERID000000000000000000,Example Workspace,01WORKSPACEID0000000000000," + ); + } +}