1use std::path::Path;
2use std::process::{Command, Stdio};
3
4use crate::config::{Config, ProviderConfig};
5use crate::error::ViaError;
6
7pub fn run(config_path: Option<&Path>, provider_name: Option<&str>) -> Result<(), ViaError> {
8 let providers = login_targets(config_path, provider_name)?;
9
10 if providers.is_empty() {
11 return Err(ViaError::InvalidConfig(
12 "no login-capable providers are configured".to_owned(),
13 ));
14 }
15
16 for provider in providers {
17 login_onepassword(&provider.name, provider.account.as_deref())?;
18 }
19
20 Ok(())
21}
22
23fn login_targets(
24 config_path: Option<&Path>,
25 provider_name: Option<&str>,
26) -> Result<Vec<OnePasswordLoginTarget>, ViaError> {
27 match Config::load(config_path) {
28 Ok(config) => onepassword_login_targets(&config, provider_name),
29 Err(ViaError::ConfigNotFound(_)) if config_path.is_none() => {
30 default_onepassword_login_targets(provider_name)
31 }
32 Err(error) => Err(error),
33 }
34}
35
36#[derive(Debug)]
37struct OnePasswordLoginTarget {
38 name: String,
39 account: Option<String>,
40}
41
42fn onepassword_login_targets(
43 config: &Config,
44 provider_name: Option<&str>,
45) -> Result<Vec<OnePasswordLoginTarget>, ViaError> {
46 if let Some(provider_name) = provider_name {
47 let provider = config.providers.get(provider_name).ok_or_else(|| {
48 ViaError::InvalidConfig(format!("provider `{provider_name}` is not configured"))
49 })?;
50 return Ok(onepassword_target(provider_name, provider)
51 .into_iter()
52 .collect());
53 }
54
55 Ok(config
56 .providers
57 .iter()
58 .filter_map(|(name, provider)| onepassword_target(name, provider))
59 .collect())
60}
61
62fn default_onepassword_login_targets(
63 provider_name: Option<&str>,
64) -> Result<Vec<OnePasswordLoginTarget>, ViaError> {
65 match provider_name {
66 Some("onepassword") | None => Ok(vec![OnePasswordLoginTarget {
67 name: "onepassword".to_owned(),
68 account: None,
69 }]),
70 Some(provider_name) => Err(ViaError::InvalidConfig(format!(
71 "provider `{provider_name}` is not configured"
72 ))),
73 }
74}
75
76fn onepassword_target(name: &str, provider: &ProviderConfig) -> Option<OnePasswordLoginTarget> {
77 match provider {
78 ProviderConfig::OnePassword { account, .. } => Some(OnePasswordLoginTarget {
79 name: name.to_owned(),
80 account: account.clone(),
81 }),
82 }
83}
84
85fn login_onepassword(provider_name: &str, account: Option<&str>) -> Result<(), ViaError> {
86 println!("provider {provider_name} (1Password): signing in");
87
88 let args = onepassword_signin_args(account);
89 let status = Command::new("op")
90 .args(&args)
91 .stdin(Stdio::inherit())
92 .stdout(Stdio::inherit())
93 .stderr(Stdio::inherit())
94 .status()
95 .map_err(|source| ViaError::MissingProgram {
96 program: "op".to_owned(),
97 source,
98 })?;
99
100 if !status.success() {
101 return Err(ViaError::ExternalCommandFailed {
102 program: "op".to_owned(),
103 status: status.code(),
104 stderr: "op signin did not complete successfully".to_owned(),
105 });
106 }
107
108 println!("provider {provider_name} (1Password): authenticated");
109 Ok(())
110}
111
112fn onepassword_signin_args(account: Option<&str>) -> Vec<String> {
113 let mut args = vec!["signin".to_owned()];
114 if let Some(account) = account {
115 args.push("--account".to_owned());
116 args.push(account.to_owned());
117 }
118 args
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 const CONFIG: &str = r#"
126version = 1
127
128[providers.onepassword]
129type = "1password"
130account = "example.1password.com"
131"#;
132
133 #[test]
134 fn builds_plain_onepassword_signin_args() {
135 assert_eq!(onepassword_signin_args(None), ["signin"]);
136 }
137
138 #[test]
139 fn builds_account_scoped_onepassword_signin_args() {
140 assert_eq!(
141 onepassword_signin_args(Some("example.1password.com")),
142 ["signin", "--account", "example.1password.com"]
143 );
144 }
145
146 #[test]
147 fn selects_configured_onepassword_provider() {
148 let config = Config::from_toml_str(CONFIG).unwrap();
149 let targets = onepassword_login_targets(&config, None).unwrap();
150
151 assert_eq!(targets.len(), 1);
152 assert_eq!(targets[0].name, "onepassword");
153 assert_eq!(targets[0].account.as_deref(), Some("example.1password.com"));
154 }
155
156 #[test]
157 fn rejects_unknown_provider() {
158 let config = Config::from_toml_str(CONFIG).unwrap();
159 let error = onepassword_login_targets(&config, Some("missing")).unwrap_err();
160
161 assert!(
162 matches!(error, ViaError::InvalidConfig(message) if message.contains("provider `missing`"))
163 );
164 }
165
166 #[test]
167 fn defaults_to_onepassword_when_config_is_missing() {
168 let targets = default_onepassword_login_targets(None).unwrap();
169
170 assert_eq!(targets.len(), 1);
171 assert_eq!(targets[0].name, "onepassword");
172 assert_eq!(targets[0].account, None);
173 }
174
175 #[test]
176 fn rejects_unknown_provider_when_config_is_missing() {
177 let error = default_onepassword_login_targets(Some("missing")).unwrap_err();
178
179 assert!(
180 matches!(error, ViaError::InvalidConfig(message) if message.contains("provider `missing`"))
181 );
182 }
183}