use std::collections::HashMap;
use std::io::Write;
use anyhow::{Context, Result};
use colored::Colorize;
use tsafe_cli::cli::{TotpAction, TotpAlgorithm};
use tsafe_core::audit::AuditEntry;
use crate::helpers::*;
pub(crate) fn cmd_qr(profile: &str, key: &str) -> Result<()> {
let vault = crate::helpers::open_vault(profile)
.with_context(|| format!("Cannot open vault '{profile}'"))?;
let value = vault
.get(key)
.with_context(|| format!("Key '{key}' not found in profile '{profile}'"))?
.clone();
drop(vault);
println!();
qr2term::print_qr(&value)
.with_context(|| "Failed to render QR code (value may be too long)")?;
println!();
eprint!("Press Enter to clear...");
let _ = std::io::stdin().read_line(&mut String::new());
print!("\x1b[2J\x1b[H");
std::io::stdout().flush()?;
audit(profile)
.append(&AuditEntry::success(profile, "qr", Some(key)))
.ok();
Ok(())
}
pub(crate) fn cmd_totp(profile: &str, action: TotpAction) -> Result<()> {
match action {
TotpAction::Add {
key,
secret,
algorithm,
digits,
period,
} => {
if digits == 0 || digits > 10 {
anyhow::bail!("--digits must be between 1 and 10 (most services use 6 or 8)");
}
if period == 0 {
anyhow::bail!("--period must be at least 1 second");
}
let base32 =
tsafe_core::totp::extract_base32(&secret).map_err(|e| anyhow::anyhow!("{e}"))?;
let mut vault = open_vault(profile)?;
let mut tags = HashMap::new();
tags.insert("type".into(), "totp".into());
let uri = format!(
"otpauth://totp/{key}?secret={}&algorithm={}&digits={}&period={}",
base32.as_str(),
algorithm.as_uri_str(),
digits,
period,
);
vault
.set(&key, &uri, tags)
.context("failed to store TOTP secret")?;
audit(profile)
.append(&AuditEntry::success(profile, "totp-add", Some(&key)))
.ok();
let algo_note =
if !matches!(algorithm, TotpAlgorithm::Sha1) || digits != 6 || period != 30 {
format!(
" (algorithm={}, digits={}, period={}s)",
algorithm.as_uri_str(),
digits,
period
)
} else {
String::new()
};
println!("{} TOTP seed stored for '{key}'{}", "✓".green(), algo_note);
Ok(())
}
TotpAction::Get { key } => {
let vault = open_vault(profile)?;
let stored = vault.get(&key).map_err(|_| {
anyhow::anyhow!(
"key '{key}' not found\n\
\n See stored TOTP keys: tsafe list --tag type=totp\
\n Add a TOTP seed: tsafe totp add {key} <base32-secret>"
)
})?;
let base32 =
tsafe_core::totp::extract_base32(&stored).map_err(|e| anyhow::anyhow!("{e}"))?;
let code =
tsafe_core::totp::generate_code(&base32).map_err(|e| anyhow::anyhow!("{e}"))?;
let secs = tsafe_core::totp::seconds_remaining();
println!("{code} ({secs}s remaining)");
audit(profile)
.append(&AuditEntry::success(profile, "totp-get", Some(&key)))
.ok();
Ok(())
}
}
}
pub(crate) fn cmd_pin(profile: &str, key: &str) -> Result<()> {
let mut vault = open_vault(profile)?;
let existing_val = vault.get(key).map_err(|_| {
anyhow::anyhow!(
"key '{key}' not found\n\
\n See all keys: tsafe list"
)
})?;
let mut tags = if let Some(entry) = vault.file().secrets.get(key) {
entry.tags.clone()
} else {
HashMap::new()
};
tags.insert("pinned".into(), "true".into());
vault.set(key, &existing_val, tags)?;
audit(profile)
.append(&AuditEntry::success(profile, "pin", Some(key)))
.ok();
println!("{} Pinned '{key}' to top of list", "✓".green());
Ok(())
}
pub(crate) fn cmd_unpin(profile: &str, key: &str) -> Result<()> {
let mut vault = open_vault(profile)?;
let existing_val = vault.get(key).map_err(|_| {
anyhow::anyhow!(
"key '{key}' not found\n\
\n See all keys: tsafe list"
)
})?;
let mut tags = if let Some(entry) = vault.file().secrets.get(key) {
entry.tags.clone()
} else {
HashMap::new()
};
tags.remove("pinned");
vault.set(key, &existing_val, tags)?;
audit(profile)
.append(&AuditEntry::success(profile, "unpin", Some(key)))
.ok();
println!("{} Unpinned '{key}'", "✓".green());
Ok(())
}