#![deny(clippy::pedantic)]
#![allow(clippy::used_underscore_binding)]
mod otp;
use clap::Clap;
use colored::Colorize;
use itertools::Itertools;
use onep_backend_api as api;
use onep_backend_op as backend;
use std::{collections::BTreeMap, convert::TryFrom};
use term_table::{
row::Row,
table_cell::{Alignment, TableCell},
Table, TableStyle,
};
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("Couldn't find the requested item.")]
NotFound,
}
#[derive(Clap, Debug)]
#[clap(author, version)]
enum Opt {
#[clap(alias = "ls")]
List {
#[clap(long, short = 'u')]
show_uuids: bool,
#[clap(long, short = 'n')]
show_account_names: bool,
},
Search {
#[clap(long, short = 'u')]
show_uuids: bool,
#[clap(long, short = 'n')]
show_account_names: bool,
terms: String,
},
#[clap(alias = "get")]
Show { uuid: String },
#[clap(alias = "gen")]
Generate {
name: String,
#[clap(long, short = 'n')]
username: Option<String>,
#[clap(long, short = 'u')]
url: Option<String>,
#[clap(long, short = 't')]
tags: Option<String>,
},
}
#[tokio::main(core_threads = 1)]
async fn main() {
if let Err(e) = run(&backend::OpBackend {}).await {
eprintln!("{}", e);
std::process::exit(1);
}
}
async fn run<T: api::Backend>(backend: &T) -> anyhow::Result<()>
where
T::Error: 'static + std::error::Error + Send + Sync,
{
match Opt::parse() {
Opt::List {
show_uuids,
show_account_names,
} => search(backend, None, show_uuids, show_account_names).await?,
Opt::Search {
terms,
show_uuids,
show_account_names,
} => search(backend, Some(terms), show_uuids, show_account_names).await?,
Opt::Show { uuid } => {
let result = backend.get(&uuid).await?.ok_or(Error::NotFound)?;
show(result);
}
Opt::Generate {
name,
username,
url,
tags,
} => {
let result = backend
.generate(&name, username.as_deref(), url.as_deref(), tags.as_deref())
.await?;
show(result);
}
}
Ok(())
}
#[allow(clippy::non_ascii_literal)]
async fn search<T: api::Backend>(
backend: &T,
terms: Option<String>,
show_uuids: bool,
show_account_names: bool,
) -> anyhow::Result<()>
where
T::Error: 'static + std::error::Error + Send + Sync,
{
let (account, vaults, results) = tokio::try_join!(
backend.account(),
backend.vaults(),
backend.search(terms.as_deref())
)?;
let mut results_grouped: BTreeMap<_, Vec<_>> = BTreeMap::new();
for (key, group) in &results.into_iter().group_by(|v| v.vault_uuid.clone()) {
results_grouped.insert(key, group.collect());
}
if let Some(terms) = terms {
if let Some(vault) = vaults
.iter()
.find(|v| v.name.to_lowercase() == terms.to_lowercase())
{
results_grouped.insert(vault.uuid.clone(), backend.search(Some(&vault.uuid)).await?);
}
}
println!("{} ({})", account.name, account.domain);
let vault_count = results_grouped.len() - 1;
for (current_vault_index, (vault, group)) in results_grouped.into_iter().enumerate() {
let vault = vaults
.iter()
.find(|v| v.uuid == vault)
.map_or_else(|| format!("Unknown Vault ({})", vault), |v| v.name.clone());
println!(
"{} {}",
if current_vault_index < vault_count {
"├──"
} else {
"└──"
},
vault.blue()
);
let line_start = if current_vault_index < vault_count {
"│"
} else {
" "
};
let item_count = group.len() - 1;
for (current_item_index, result) in group.into_iter().enumerate() {
println!(
"{} {} {}",
line_start,
if current_item_index < item_count {
"├──"
} else {
"└──"
},
result.title.trim()
);
let prefix = if current_item_index < item_count {
"│ "
} else {
" "
};
if show_account_names && !result.account_info.trim().is_empty() {
println!(
"{} {} {}",
line_start,
prefix,
result.account_info.trim().green()
);
}
if show_uuids {
println!("{} {} {}", line_start, prefix, result.uuid.yellow());
}
}
}
Ok(())
}
fn show(item: api::Item) {
let mut table = Table::new();
table.style = TableStyle::extended();
table.add_row(Row::new(vec![TableCell::new_with_alignment(
item.title,
2,
Alignment::Center,
)]));
for field in item.fields {
table.add_row(Row::new(vec![
TableCell::new(field.name),
TableCell::new_with_alignment(field.value, 1, Alignment::Right),
]));
}
println!("{}", table.render());
for section in item.sections {
if section.fields.is_empty() {
continue;
}
let mut table = Table::new();
table.style = TableStyle::extended();
if !section.name.is_empty() {
table.add_row(Row::new(vec![TableCell::new_with_alignment(
section.name,
2,
Alignment::Center,
)]));
}
for field in section.fields {
let mut value = field.value;
if field.field_type == api::ItemFieldType::Totp {
if let Ok(tfa) = otp::TwoFactorAuth::try_from(value.as_ref()) {
value = tfa.generate().value;
}
}
table.add_row(Row::new(vec![
TableCell::new(field.name),
TableCell::new_with_alignment(value, 1, Alignment::Right),
]));
}
println!("{}", table.render());
}
}