pimalaya_cli/wizard/
carddav.rs1use secrecy::SecretString;
4
5use crate::prompt::{self, PromptResult};
6
7#[derive(Clone, Debug, Default)]
15pub struct WizardCarddavConfig {
16 pub account_name: String,
18 pub project_name: String,
20 pub email: Option<String>,
22 pub auth: CarddavAuth,
24 pub bearer_only: bool,
27}
28
29#[derive(Clone, Debug)]
31pub enum CarddavAuth {
32 Basic {
34 username: String,
36 secret: CarddavSecret,
38 },
39 Bearer {
41 secret: CarddavSecret,
43 },
44}
45
46impl Default for CarddavAuth {
47 fn default() -> Self {
48 Self::Basic {
49 username: String::new(),
50 secret: CarddavSecret::default(),
51 }
52 }
53}
54
55#[derive(Clone, Debug)]
58pub enum CarddavSecret {
59 Raw(SecretString),
61 Command(String),
63}
64
65impl Default for CarddavSecret {
66 fn default() -> Self {
67 Self::Command(String::new())
68 }
69}
70
71const CMD: &str = "Use a shell command to retrieve my secret (recommended)";
72const RAW: &str = "Save secret in the configuration file (plaintext, NOT recommended)";
73const SECRETS: [&str; 2] = [CMD, RAW];
74
75const BASIC: &str = "Basic (username + password)";
76const BEARER: &str = "Bearer (token)";
77const AUTHS: [&str; 2] = [BASIC, BEARER];
78
79pub fn run(defaults: &WizardCarddavConfig) -> PromptResult<WizardCarddavConfig> {
82 let strategies: &[&str] = if defaults.bearer_only {
85 &[BEARER]
86 } else {
87 &AUTHS
88 };
89
90 let default_strategy = match &defaults.auth {
91 CarddavAuth::Basic { .. } => BASIC,
92 CarddavAuth::Bearer { .. } => BEARER,
93 };
94
95 let strategy = prompt::item(
96 "CardDAV authentication strategy:",
97 strategies.iter().copied(),
98 Some(default_strategy),
99 )?;
100
101 let auth = match strategy {
102 BASIC => {
103 let default_username = match &defaults.auth {
104 CarddavAuth::Basic { username, .. } if !username.is_empty() => {
105 Some(username.clone())
106 }
107 _ => defaults.email.clone(),
108 };
109
110 let username = prompt::text("CardDAV username:", default_username.as_deref())?;
111 let secret = prompt_secret(
112 &defaults.account_name,
113 &defaults.project_name,
114 "password",
115 auth_secret(&defaults.auth),
116 )?;
117
118 CarddavAuth::Basic { username, secret }
119 }
120 BEARER => {
121 let secret = prompt_secret(
122 &defaults.account_name,
123 &defaults.project_name,
124 "token",
125 auth_secret(&defaults.auth),
126 )?;
127 CarddavAuth::Bearer { secret }
128 }
129 _ => unreachable!(),
130 };
131
132 Ok(WizardCarddavConfig {
133 account_name: defaults.account_name.clone(),
134 project_name: defaults.project_name.clone(),
135 email: defaults.email.clone(),
136 auth,
137 bearer_only: defaults.bearer_only,
138 })
139}
140
141fn auth_secret(auth: &CarddavAuth) -> &CarddavSecret {
144 match auth {
145 CarddavAuth::Basic { secret, .. } => secret,
146 CarddavAuth::Bearer { secret } => secret,
147 }
148}
149
150fn prompt_secret(
151 account_name: &str,
152 project_name: &str,
153 label: &str,
154 default: &CarddavSecret,
155) -> PromptResult<CarddavSecret> {
156 let default_strategy = match default {
157 CarddavSecret::Command(_) => CMD,
158 CarddavSecret::Raw(_) => RAW,
159 };
160
161 let strategy = prompt::item("CardDAV secret strategy:", SECRETS, Some(default_strategy))?;
162
163 match strategy {
164 CMD => {
165 let default_cmd = match default {
166 CarddavSecret::Command(cmd) if !cmd.is_empty() => cmd.clone(),
167 _ => default_secret_cmd(account_name, project_name, "carddav"),
168 };
169 let cmd = prompt::text("Shell command:", Some(&default_cmd))?;
170 Ok(CarddavSecret::Command(cmd))
171 }
172 RAW => {
173 let secret = prompt::password(
174 format!("CardDAV {label}:"),
175 format!("Confirm CardDAV {label}:"),
176 )?;
177 Ok(CarddavSecret::Raw(secret))
178 }
179 _ => unreachable!(),
180 }
181}
182
183fn default_secret_cmd(account_name: &str, project_name: &str, protocol: &str) -> String {
184 if cfg!(target_os = "macos") {
185 format!(
186 "security find-generic-password \
187 -a '{account_name}' \
188 -s '{project_name}-{account_name}-{protocol}' \
189 -w"
190 )
191 } else if cfg!(target_os = "linux") {
192 format!("secret-tool lookup account {account_name} service {project_name}-{protocol}")
193 } else {
194 String::new()
195 }
196}