From bb6bed55311a2e9e9bcc82399ea6ef1c744cd712 Mon Sep 17 00:00:00 2001 From: Ty Date: Wed, 2 Aug 2023 19:16:37 -0600 Subject: [PATCH] Add keys delete subcommand --- .vscode/aspm.code-snippets | 41 ++++++++++++++++ src/commands/keys/delete.rs | 98 +++++++++++++++++++++++++++++++++++++ src/commands/keys/mod.rs | 2 + src/commands/mod.rs | 6 +-- src/main.rs | 13 ++--- 5 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 .vscode/aspm.code-snippets create mode 100644 src/commands/keys/delete.rs diff --git a/.vscode/aspm.code-snippets b/.vscode/aspm.code-snippets new file mode 100644 index 0000000..2540686 --- /dev/null +++ b/.vscode/aspm.code-snippets @@ -0,0 +1,41 @@ +{ + // Place your aspm workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Create aspm subcommand": { + "scope": "rust", + "prefix": "subcommand", + "body": [ + "use clap::Parser;", + "", + "use crate::commands::AspmSubcommand;", + "", + "/// $2", + "#[derive(Parser, Debug)]", + "pub struct $1Command { }", + "", + "#[async_trait::async_trait]", + "impl AspmSubcommand for $1Command {", + "\tasync fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {", + "\t\t", + "\t\t", + "\t\tOk(())", + "\t}", + "}", + ], + "description": "Create a template for an aspm subcommand" + } +} \ No newline at end of file diff --git a/src/commands/keys/delete.rs b/src/commands/keys/delete.rs new file mode 100644 index 0000000..ba38ff5 --- /dev/null +++ b/src/commands/keys/delete.rs @@ -0,0 +1,98 @@ +use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle}; +use anyhow::Context; +use asp::keys::AspKeyType; +use clap::Parser; +use dialoguer::{Confirm, theme::ColorfulTheme, console::Term}; +use indoc::writedoc; +use sea_orm::ModelTrait; + +use std::io::Write; + +use crate::{ + entities::keys::{Entity as KeysEntity, Model as KeysModel}, + commands::{AspmSubcommand, KeysQueryResult, KeysEntityExt} +}; + +/// Deletes a saved key, after asking for confirmation. +#[derive(Parser, Debug)] +pub struct KeysDeleteCommand { + /// Will disable any confirmation prompts. This is not recommended unless you knowingly give a full fingerprint, as it is easy to accidentally (and permanently) delete wrong keys otherwise. + #[arg(long)] + no_confirm: bool, + /// The key to export. This can either be a fingerprint obtained with the `keys list` command, or an alias to search for. + key: String, +} + +#[async_trait::async_trait] +impl AspmSubcommand for KeysDeleteCommand { + async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> { + // Fetch key from db + let entry = KeysEntity::query_key(&state.db, &self.key) + .await + .context("Unable to query keys from database")?; + let key: KeysModel = match entry { + KeysQueryResult::None => { + eprintln!( + "{style}No keys matching the given query were found{reset}", + style = Anstyle::new() + .fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightRed))) + .render(), + reset = Reset.render() + ); + std::process::exit(1); + } + KeysQueryResult::One(key) => key, + KeysQueryResult::Many(mut keys) => { + eprintln!( + "{style}More than one keys matching the given query were found{reset}", + style = Anstyle::new() + .fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightYellow))) + .render(), + reset = Reset::default().render() + ); + keys.remove(0) + } + }; + + if !self.no_confirm { + // Construct styles + let reset = Reset::default().render(); + let alias_style = Anstyle::new() + .underline() + .fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightCyan))) + .render(); + let key_style = Anstyle::new() + .fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightGreen))) + .render(); + let value_style = Anstyle::new() + .fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightYellow))) + .render(); + let _ = writedoc! { + std::io::stderr(), + " + {alias_style}{alias}:{reset} + {key_style}Fingerprint{reset} {value_style}{fingerprint}{reset} + {key_style}Key Type{reset} {value_style}{key_type:?}{reset}\n + ", + fingerprint = key.fingerprint, + key_type = TryInto::::try_into(key.key_type).context("Unable to get key type from database")?, + alias = key.alias + }; + + let confirmation = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Are you sure you want to delete this key?") + .interact_on(&Term::stderr()) + .context("Unable to prompt on stderr")?; + + if !confirmation { + std::process::exit(1); + } + } + + let fingerprint = key.fingerprint.clone(); + key.delete(&state.db).await.context("Unable to delete key")?; + println!("Successfully deleted key with fingerprint {}", fingerprint); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/commands/keys/mod.rs b/src/commands/keys/mod.rs index 61d601a..6390513 100644 --- a/src/commands/keys/mod.rs +++ b/src/commands/keys/mod.rs @@ -5,6 +5,7 @@ pub mod generate; #[cfg(feature = "gpg-compat")] pub mod import_gpg; pub mod list; +pub mod delete; /// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles. #[derive(Parser)] @@ -20,4 +21,5 @@ pub enum KeysSubcommands { Export(export::KeysExportCommand), #[cfg(feature = "gpg-compat")] ImportGpg(import_gpg::KeysImportGpgCommand), + Delete(delete::KeysDeleteCommand), } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index da24253..72c7b92 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,12 +8,12 @@ use crate::entities::keys::{Column as KeysColumn, Entity as KeysEntity, Model as use crate::AspmState; #[async_trait::async_trait] -pub trait AspmSubcommand: Parser { +pub trait AspmSubcommand: Parser + Sync { async fn execute(&self, _state: AspmState) -> Result<(), anyhow::Error> { panic!("Not implemented") } - fn execute_sync(&self, _state: AspmState, _runtime: Runtime) -> Result<(), anyhow::Error> { - panic!("Not implemented") + fn execute_sync(&self, state: AspmState, runtime: Runtime) -> Result<(), anyhow::Error> { + runtime.block_on(async { self.execute(state).await }) } } diff --git a/src/main.rs b/src/main.rs index e724ed5..7748aa3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -139,15 +139,10 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> { // Call the subcommand match &parsed.subcommand { AspmSubcommands::Keys(subcommand) => match &subcommand.subcommand { - KeysSubcommands::Generate(subcommand) => { - runtime.block_on(async { subcommand.execute(state).await }) - } - KeysSubcommands::List(subcommand) => { - runtime.block_on(async { subcommand.execute(state).await }) - } - KeysSubcommands::Export(subcommand) => { - runtime.block_on(async { subcommand.execute(state).await }) - } + KeysSubcommands::Generate(subcommand) => subcommand.execute_sync(state, runtime), + KeysSubcommands::List(subcommand) => subcommand.execute_sync(state, runtime), + KeysSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime), + KeysSubcommands::Delete(subcommand) => subcommand.execute_sync(state, runtime), #[cfg(feature = "gpg-compat")] KeysSubcommands::ImportGpg(subcommand) => subcommand.execute_sync(state, runtime), },