diff --git a/README.md b/README.md index 9c34cff..0a6a6a2 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,12 @@ blackcandy config The config file is written with user-only permissions on Unix systems. +To revoke the current server session and remove the stored credentials, log out: + +```sh +blackcandy logout +``` + For isolated test runs, point `BLACKCANDY_CONFIG` at a temporary file: ```sh diff --git a/src/api.rs b/src/api.rs index 34b2be9..5887adc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -46,6 +46,10 @@ impl ApiClient { .await } + pub async fn logout(&self) -> Result<()> { + self.delete_empty("my/session").await + } + pub async fn system(&self) -> Result { self.get_without_auth("system", &[]).await } diff --git a/src/config.rs b/src/config.rs index 4d19fe4..cd46c72 100644 --- a/src/config.rs +++ b/src/config.rs @@ -59,6 +59,19 @@ impl Config { } } +/// Remove the stored config file. Returns `true` if a file was deleted and +/// `false` if there was nothing to remove. +pub fn remove_config() -> Result { + let path = config_path()?; + if !path.exists() { + return Ok(false); + } + + fs::remove_file(&path) + .with_context(|| format!("failed to remove config at {}", path.display()))?; + Ok(true) +} + pub fn config_path() -> Result { if let Ok(path) = std::env::var("BLACKCANDY_CONFIG") { return Ok(PathBuf::from(path)); diff --git a/src/main.rs b/src/main.rs index 1841b4a..e119298 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::io::{self, Write}; use crate::{ api::{ApiClient, SongQuery, validate_limit}, - config::{Config, config_path}, + config::{Config, config_path, remove_config}, }; #[derive(Debug, Parser)] @@ -25,6 +25,8 @@ struct Cli { enum Command { /// Log in to a Black Candy server and store an API token. Login(LoginArgs), + /// Log out by removing the stored credentials. + Logout, /// Show the configured server and current config path. Config, /// Show Black Candy server version information. @@ -194,6 +196,7 @@ async fn main() -> Result<()> { match cli.command { Command::Login(args) => login(args).await, + Command::Logout => logout().await, Command::Config => show_config(), command => { let config = Config::load()?; @@ -237,6 +240,24 @@ async fn login(args: LoginArgs) -> Result<()> { Ok(()) } +async fn logout() -> Result<()> { + let config = Config::load()?; + if config.api_token.is_some() { + let client = configured_client(&config)?; + client.logout().await?; + } + + if remove_config()? { + println!( + "Logged out. Removed config at {}.", + config_path()?.display() + ); + } else { + println!("Not logged in; nothing to remove."); + } + Ok(()) +} + fn prompt_line(prompt: &str) -> Result { print!("{prompt}"); io::stdout().flush()?; @@ -299,7 +320,9 @@ async fn run_authenticated(command: Command, config: Config, client: ApiClient) Command::Queue(args) => run_queue(args, &client).await?, Command::Favorite(args) => run_favorite(args, &client).await?, Command::Playlist(args) => run_playlist(args, &client).await?, - Command::Login(_) | Command::Config => unreachable!("handled before authentication setup"), + Command::Login(_) | Command::Logout | Command::Config => { + unreachable!("handled before authentication setup") + } } Ok(())