use secrecy::SecretString;
use crate::prompt::{self, PromptResult};
#[derive(Clone, Debug, Default)]
pub struct WizardCarddavConfig {
pub account_name: String,
pub project_name: String,
pub email: Option<String>,
pub auth: CarddavAuth,
pub bearer_only: bool,
}
#[derive(Clone, Debug)]
pub enum CarddavAuth {
Basic {
username: String,
secret: CarddavSecret,
},
Bearer {
secret: CarddavSecret,
},
}
impl Default for CarddavAuth {
fn default() -> Self {
Self::Basic {
username: String::new(),
secret: CarddavSecret::default(),
}
}
}
#[derive(Clone, Debug)]
pub enum CarddavSecret {
Raw(SecretString),
Command(String),
}
impl Default for CarddavSecret {
fn default() -> Self {
Self::Command(String::new())
}
}
const CMD: &str = "Use a shell command to retrieve my secret (recommended)";
const RAW: &str = "Save secret in the configuration file (plaintext, NOT recommended)";
const SECRETS: [&str; 2] = [CMD, RAW];
const BASIC: &str = "Basic (username + password)";
const BEARER: &str = "Bearer (token)";
const AUTHS: [&str; 2] = [BASIC, BEARER];
pub fn run(defaults: &WizardCarddavConfig) -> PromptResult<WizardCarddavConfig> {
let strategies: &[&str] = if defaults.bearer_only {
&[BEARER]
} else {
&AUTHS
};
let default_strategy = match &defaults.auth {
CarddavAuth::Basic { .. } => BASIC,
CarddavAuth::Bearer { .. } => BEARER,
};
let strategy = prompt::item(
"CardDAV authentication strategy:",
strategies.iter().copied(),
Some(default_strategy),
)?;
let auth = match strategy {
BASIC => {
let default_username = match &defaults.auth {
CarddavAuth::Basic { username, .. } if !username.is_empty() => {
Some(username.clone())
}
_ => defaults.email.clone(),
};
let username = prompt::text("CardDAV username:", default_username.as_deref())?;
let secret = prompt_secret(
&defaults.account_name,
&defaults.project_name,
"password",
auth_secret(&defaults.auth),
)?;
CarddavAuth::Basic { username, secret }
}
BEARER => {
let secret = prompt_secret(
&defaults.account_name,
&defaults.project_name,
"token",
auth_secret(&defaults.auth),
)?;
CarddavAuth::Bearer { secret }
}
_ => unreachable!(),
};
Ok(WizardCarddavConfig {
account_name: defaults.account_name.clone(),
project_name: defaults.project_name.clone(),
email: defaults.email.clone(),
auth,
bearer_only: defaults.bearer_only,
})
}
fn auth_secret(auth: &CarddavAuth) -> &CarddavSecret {
match auth {
CarddavAuth::Basic { secret, .. } => secret,
CarddavAuth::Bearer { secret } => secret,
}
}
fn prompt_secret(
account_name: &str,
project_name: &str,
label: &str,
default: &CarddavSecret,
) -> PromptResult<CarddavSecret> {
let default_strategy = match default {
CarddavSecret::Command(_) => CMD,
CarddavSecret::Raw(_) => RAW,
};
let strategy = prompt::item("CardDAV secret strategy:", SECRETS, Some(default_strategy))?;
match strategy {
CMD => {
let default_cmd = match default {
CarddavSecret::Command(cmd) if !cmd.is_empty() => cmd.clone(),
_ => default_secret_cmd(account_name, project_name, "carddav"),
};
let cmd = prompt::text("Shell command:", Some(&default_cmd))?;
Ok(CarddavSecret::Command(cmd))
}
RAW => {
let secret = prompt::password(
format!("CardDAV {label}:"),
format!("Confirm CardDAV {label}:"),
)?;
Ok(CarddavSecret::Raw(secret))
}
_ => unreachable!(),
}
}
fn default_secret_cmd(account_name: &str, project_name: &str, protocol: &str) -> String {
if cfg!(target_os = "macos") {
format!(
"security find-generic-password \
-a '{account_name}' \
-s '{project_name}-{account_name}-{protocol}' \
-w"
)
} else if cfg!(target_os = "linux") {
format!("secret-tool lookup account {account_name} service {project_name}-{protocol}")
} else {
String::new()
}
}