romm_cli/tui/screens/setup_wizard/
config.rs1use anyhow::{anyhow, Context, Result};
4
5use crate::client::RommClient;
6use crate::config::{
7 default_theme_id, is_keyring_placeholder, load_config, normalize_romm_origin,
8 persist_user_config, read_user_config_json_from_disk, AuthConfig, Config, RomsLayoutConfig,
9};
10use crate::core::download::validate_configured_download_directory;
11use crate::endpoints::client_tokens::ExchangeClientToken;
12use crate::tui::path_picker::{PathPicker, PathPickerMode};
13
14use super::layout::extras_defaults_from_disk;
15use super::types::{AuthKind, SetupWizard, Step};
16
17impl SetupWizard {
18 pub fn new() -> Self {
19 let default_dl = dirs::download_dir()
20 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join("Downloads"))
21 .join("romm-cli")
22 .display()
23 .to_string();
24 Self {
25 step: Step::Url,
26 auth_kind: AuthKind::Pairing,
27 auth_menu_selected: 0,
28 url: "https://".to_string(),
29 url_cursor: "https://".len(),
30 download_picker: PathPicker::new(PathPickerMode::Directory, &default_dl),
31 username: String::new(),
32 user_cursor: 0,
33 password: String::new(),
34 bearer_token: String::new(),
35 bearer_cursor: 0,
36 api_header: String::new(),
37 header_cursor: 0,
38 api_key: String::new(),
39 api_key_cursor: 0,
40 pairing_code: String::new(),
41 pairing_cursor: 0,
42 reuse_keyring_password: false,
43 reuse_keyring_bearer: false,
44 reuse_keyring_api_key: false,
45 testing: false,
46 use_https: true,
47 skip_custom_console_paths: false,
48 error: None,
49 }
50 }
51
52 pub fn new_auth_only(config: &Config) -> Self {
53 let mut wizard = Self::new();
54 wizard.step = Step::AuthMenu;
55 wizard.url = config.base_url.clone();
56 wizard
57 .download_picker
58 .set_path_text(config.download_dir.clone());
59 wizard.use_https = config.use_https;
60 wizard.skip_custom_console_paths = true;
61
62 let disk = read_user_config_json_from_disk();
63
64 match &config.auth {
65 Some(AuthConfig::Basic { username, password }) => {
66 wizard.auth_kind = AuthKind::Basic;
67 wizard.auth_menu_selected = 1;
68 wizard.username = username.clone();
69 wizard.user_cursor = username.len();
70 let disk_pass = disk
71 .as_ref()
72 .and_then(|c| c.auth.as_ref())
73 .and_then(|a| match a {
74 AuthConfig::Basic { password, .. } => Some(password.as_str()),
75 _ => None,
76 });
77 if disk_pass.is_some_and(is_keyring_placeholder) {
78 wizard.password = String::new();
79 wizard.reuse_keyring_password = true;
80 } else {
81 wizard.password = password.clone();
82 }
83 }
84 Some(AuthConfig::Bearer { token }) => {
85 wizard.auth_kind = AuthKind::Bearer;
86 wizard.auth_menu_selected = 2;
87 let disk_tok = disk
88 .as_ref()
89 .and_then(|c| c.auth.as_ref())
90 .and_then(|a| match a {
91 AuthConfig::Bearer { token } => Some(token.as_str()),
92 _ => None,
93 });
94 if disk_tok.is_some_and(is_keyring_placeholder) {
95 wizard.bearer_token = String::new();
96 wizard.bearer_cursor = 0;
97 wizard.reuse_keyring_bearer = true;
98 } else {
99 wizard.bearer_token = token.clone();
100 wizard.bearer_cursor = token.len();
101 }
102 }
103 Some(AuthConfig::ApiKey { header, key }) => {
104 wizard.auth_kind = AuthKind::ApiKey;
105 wizard.auth_menu_selected = 3;
106 wizard.api_header = header.clone();
107 wizard.header_cursor = header.len();
108 let disk_key = disk
109 .as_ref()
110 .and_then(|c| c.auth.as_ref())
111 .and_then(|a| match a {
112 AuthConfig::ApiKey { key, .. } => Some(key.as_str()),
113 _ => None,
114 });
115 if disk_key.is_some_and(is_keyring_placeholder) {
116 wizard.api_key = String::new();
117 wizard.api_key_cursor = 0;
118 wizard.reuse_keyring_api_key = true;
119 } else {
120 wizard.api_key = key.clone();
121 wizard.api_key_cursor = key.len();
122 }
123 }
124 None => {
125 wizard.auth_kind = AuthKind::Pairing;
126 wizard.auth_menu_selected = 0;
127 }
128 }
129 wizard
130 }
131
132 pub(crate) fn auth_labels() -> [&'static str; 4] {
133 [
134 "Pair with Web UI (8-character code) (Recommended)",
135 "Username + password",
136 "API Token",
137 "API key in custom header",
138 ]
139 }
140
141 pub(crate) fn auth_kind_from_index(i: usize) -> AuthKind {
142 match i {
143 0 => AuthKind::Pairing,
144 1 => AuthKind::Basic,
145 2 => AuthKind::Bearer,
146 _ => AuthKind::ApiKey,
147 }
148 }
149
150 fn roms_layout_from_wizard(&self) -> RomsLayoutConfig {
151 read_user_config_json_from_disk()
152 .map(|c| c.roms_layout)
153 .unwrap_or_default()
154 }
155
156 pub(crate) async fn pairing_config_from_exchange(&self, verbose: bool) -> Result<Config> {
158 let base_url = normalize_romm_origin(self.url.trim());
159 if base_url.is_empty() {
160 return Err(anyhow!("Server URL cannot be empty"));
161 }
162 let code = self.pairing_code.trim().to_string();
163 if code.is_empty() {
164 return Err(anyhow!("Pairing code cannot be empty"));
165 }
166 let download_dir =
167 validate_configured_download_directory(self.download_picker.path_trimmed().trim())?
168 .display()
169 .to_string();
170 let temp_config = Config {
171 base_url: base_url.clone(),
172 download_dir: download_dir.clone(),
173 use_https: self.use_https,
174 auth: None,
175 extras_defaults: extras_defaults_from_disk(),
176 save_sync: read_user_config_json_from_disk()
177 .map(|c| c.save_sync)
178 .unwrap_or_default(),
179 roms_layout: self.roms_layout_from_wizard(),
180 theme: read_user_config_json_from_disk()
181 .map(|c| c.theme)
182 .unwrap_or_else(default_theme_id),
183 };
184 let client = RommClient::new(&temp_config, verbose)?;
185 let response = client
186 .call(&ExchangeClientToken { code })
187 .await
188 .context("failed to exchange pairing code")?;
189 Ok(Config {
190 base_url,
191 download_dir,
192 use_https: self.use_https,
193 auth: Some(AuthConfig::Bearer {
194 token: response.raw_token,
195 }),
196 extras_defaults: extras_defaults_from_disk(),
197 save_sync: read_user_config_json_from_disk()
198 .map(|c| c.save_sync)
199 .unwrap_or_default(),
200 roms_layout: self.roms_layout_from_wizard(),
201 theme: read_user_config_json_from_disk()
202 .map(|c| c.theme)
203 .unwrap_or_else(default_theme_id),
204 })
205 }
206
207 fn build_config(&self) -> Result<Config> {
208 let base_url = normalize_romm_origin(self.url.trim());
209 if base_url.is_empty() {
210 return Err(anyhow!("Server URL cannot be empty"));
211 }
212 let download_dir =
213 validate_configured_download_directory(self.download_picker.path_trimmed().trim())?
214 .display()
215 .to_string();
216 let auth: Option<AuthConfig> = match self.auth_kind {
217 AuthKind::Basic => {
218 let u = self.username.trim();
219 if u.is_empty() {
220 return Err(anyhow!("Username cannot be empty"));
221 }
222 let password = if self.password.is_empty() && self.reuse_keyring_password {
223 crate::config::keyring_get("API_PASSWORD").ok_or_else(|| {
224 anyhow!("Password not in OS keyring; enter a password or run romm-cli init")
225 })?
226 } else if self.password.is_empty() {
227 return Err(anyhow!("Password cannot be empty"));
228 } else {
229 self.password.clone()
230 };
231 Some(AuthConfig::Basic {
232 username: u.to_string(),
233 password,
234 })
235 }
236 AuthKind::Bearer => {
237 let token = if self.bearer_token.trim().is_empty() && self.reuse_keyring_bearer {
238 crate::config::keyring_get("API_TOKEN").ok_or_else(|| {
239 anyhow!("API token not in OS keyring; enter a token or run romm-cli init")
240 })?
241 } else if self.bearer_token.trim().is_empty() {
242 return Err(anyhow!("Bearer token cannot be empty"));
243 } else {
244 self.bearer_token.trim().to_string()
245 };
246 Some(AuthConfig::Bearer { token })
247 }
248 AuthKind::ApiKey => {
249 let h = self.api_header.trim();
250 if h.is_empty() {
251 return Err(anyhow!("Header name cannot be empty"));
252 }
253 let key = if self.api_key.is_empty() && self.reuse_keyring_api_key {
254 crate::config::keyring_get("API_KEY").ok_or_else(|| {
255 anyhow!("API key not in OS keyring; enter a key or run romm-cli init")
256 })?
257 } else if self.api_key.is_empty() {
258 return Err(anyhow!("API key cannot be empty"));
259 } else {
260 self.api_key.clone()
261 };
262 Some(AuthConfig::ApiKey {
263 header: h.to_string(),
264 key,
265 })
266 }
267 AuthKind::Pairing => {
268 return Err(anyhow!(
269 "Pairing auth is applied when connecting; use the pairing code step and connect"
270 ));
271 }
272 };
273 Ok(Config {
274 base_url,
275 download_dir,
276 use_https: self.use_https,
277 auth,
278 extras_defaults: extras_defaults_from_disk(),
279 save_sync: read_user_config_json_from_disk()
280 .map(|c| c.save_sync)
281 .unwrap_or_default(),
282 roms_layout: self.roms_layout_from_wizard(),
283 theme: read_user_config_json_from_disk()
284 .map(|c| c.theme)
285 .unwrap_or_else(default_theme_id),
286 })
287 }
288
289 pub async fn try_connect_and_persist(&mut self, verbose: bool) -> Result<Config> {
290 let cfg = if self.auth_kind == AuthKind::Pairing {
291 self.pairing_config_from_exchange(verbose).await?
292 } else {
293 self.build_config()?
294 };
295 let client = RommClient::new(&cfg, verbose)?;
296 client.fetch_openapi_json().await?;
297 persist_user_config(&cfg)?;
298 load_config().map_err(Into::into)
299 }
300}