use std::collections::HashMap;
use std::io::Write;
use crate::cli::{TotpAction, TotpAlgorithm};
use anyhow::{Context, Result};
use colored::Colorize;
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.is_some_and(|digits| digits == 0 || digits > 10) {
anyhow::bail!("--digits must be between 1 and 10 (most services use 6 or 8)");
}
if period.is_some_and(|period| period == 0) {
anyhow::bail!("--period must be at least 1 second");
}
let mut vault = open_vault(profile)?;
let mut tags = HashMap::new();
tags.insert("type".into(), "totp".into());
let algorithm_name = algorithm.map(TotpAlgorithm::as_uri_str);
let uri =
tsafe_core::totp::provisioning_uri(&key, &secret, algorithm_name, digits, period)
.map_err(|e| anyhow::anyhow!("{e}"))?;
vault
.set(&key, &uri, tags)
.context("failed to store TOTP secret")?;
audit(profile)
.append(&AuditEntry::success(profile, "totp-add", Some(&key)))
.ok();
println!("{} TOTP seed stored for '{key}'", "✓".green());
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 code =
tsafe_core::totp::generate_code(&stored).map_err(|e| anyhow::anyhow!("{e}"))?;
let secs = tsafe_core::totp::seconds_remaining_for(&stored)
.map_err(|e| anyhow::anyhow!("{e}"))?;
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(())
}