use std::convert::Infallible;
use std::io::Read;
use std::str::FromStr;
use std::sync::Arc;
use atuin_client::record::sqlite_store::SqliteStore;
use atuin_client::settings::Settings;
use atuin_common::env::var_nonempty;
use clap::{Args, Subcommand};
use eyre::{Context, Result};
use secrecy::SecretString;
use secrecy::zeroize::Zeroizing;
use tracing::instrument;
use crate::i18n::fl;
pub mod change_password;
pub mod delete;
pub mod link;
pub mod login;
pub mod logout;
pub mod register;
const PASSWORD_ENV: &str = "ATUIN_PASSWORD";
#[derive(Clone, Debug)]
pub enum PasswordArg {
Stdin,
Value(SecretString),
}
impl FromStr for PasswordArg {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"-" => Self::Stdin,
password => Self::Value(password.into()),
})
}
}
impl PasswordArg {
pub fn resolve(arg: Option<&Self>, mut stdin: impl Read) -> Result<Option<SecretString>> {
let Some(arg) = arg else {
return Ok(var_nonempty(PASSWORD_ENV)
.and_then(|password| password.into_string().ok())
.map(SecretString::from));
};
match arg {
Self::Value(password) => Ok(Some(password.clone())),
Self::Stdin => {
let mut buf = Zeroizing::new(String::new());
stdin.read_to_string(&mut buf).context(fl!("account-password-stdin-failed"))?;
Ok(Some(buf.trim_end_matches(['\r', '\n']).into()))
}
}
}
}
#[derive(Args, Debug)]
pub struct Cmd {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(about = fl!("cmd-login"))]
Login(login::Cmd),
#[command(about = fl!("cmd-account-register"))]
Register(register::Cmd),
#[command(about = fl!("cmd-logout"))]
Logout,
#[command(about = fl!("cmd-account-delete"))]
Delete(delete::Cmd),
#[command(about = fl!("cmd-account-change-password"))]
ChangePassword(change_password::Cmd),
#[command(about = fl!("cmd-account-link"))]
Link,
}
impl Cmd {
#[instrument(level = "trace", skip_all, err)]
pub async fn run(self, settings: &Arc<Settings>, store: SqliteStore) -> Result<()> {
match self.command {
Commands::Login(l) => l.run(settings, &store).await,
Commands::Register(r) => r.run(settings, &store).await,
Commands::Logout => logout::run(settings).await,
Commands::Delete(d) => d.run(settings).await,
Commands::ChangePassword(c) => c.run(settings).await,
Commands::Link => link::run(settings).await,
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use secrecy::ExposeSecret;
use super::*;
#[rstest]
#[case::value("hunter2", "stdin", "hunter2")]
#[case::stdin("-", "hunter2\n", "hunter2")]
#[case::stdin_crlf("-", "hunter2\r\n", "hunter2")]
#[case::stdin_keeps_inner_whitespace("-", " hunter 2\n", " hunter 2")]
fn resolve_reads_value_or_stdin(
#[case] arg: &str,
#[case] stdin: &str,
#[case] expected: &str,
) {
let arg: PasswordArg = arg.parse().unwrap();
let password = PasswordArg::resolve(Some(&arg), stdin.as_bytes()).unwrap();
assert_eq!(password.as_ref().map(ExposeSecret::expose_secret), Some(expected));
}
}