1use std::path::Path;
8
9use ito_config::{ConfigContext, global_config_path, load_global_ito_config};
10
11use crate::errors::{CoreError, CoreResult};
12
13#[derive(Debug)]
19pub enum InitAuthResult {
20 AlreadyConfigured {
22 config_path: String,
24 },
25 Generated {
27 config_path: String,
29 },
30}
31
32pub fn init_backend_auth(ctx: &ConfigContext) -> CoreResult<InitAuthResult> {
41 let global_config = load_global_ito_config(ctx);
42 let existing_auth = &global_config.backend_server.auth;
43
44 let has_tokens = existing_auth
45 .admin_tokens
46 .iter()
47 .any(|t| !t.trim().is_empty());
48
49 let config_display = global_config_display_path(ctx);
50
51 if has_tokens {
52 return Ok(InitAuthResult::AlreadyConfigured {
53 config_path: config_display,
54 });
55 }
56
57 let admin_token = crate::token::generate_token();
58 let token_seed = crate::token::generate_token();
59
60 write_auth_to_global_config(ctx, &admin_token, &token_seed)?;
61
62 Ok(InitAuthResult::Generated {
63 config_path: config_display,
64 })
65}
66
67pub fn resolve_admin_tokens(cli_tokens: &[String], config_tokens: &[String]) -> Vec<String> {
76 let mut tokens: Vec<String> = cli_tokens.to_vec();
77
78 if let Ok(env_token) = std::env::var("ITO_BACKEND_ADMIN_TOKEN") {
79 let trimmed = env_token.trim().to_string();
80 if !trimmed.is_empty() && !tokens.contains(&trimmed) {
81 tokens.push(trimmed);
82 }
83 }
84
85 for token in config_tokens {
86 let trimmed = token.trim().to_string();
87 if !trimmed.is_empty() && !tokens.contains(&trimmed) {
88 tokens.push(trimmed);
89 }
90 }
91
92 tokens
93}
94
95pub fn resolve_token_seed(
98 cli_seed: &Option<String>,
99 config_seed: &Option<String>,
100) -> Option<String> {
101 if let Some(seed) = cli_seed.as_ref().filter(|s| !s.trim().is_empty()) {
102 return Some(seed.clone());
103 }
104
105 if let Ok(env_seed) = std::env::var("ITO_BACKEND_TOKEN_SEED") {
106 let trimmed = env_seed.trim().to_string();
107 if !trimmed.is_empty() {
108 return Some(trimmed);
109 }
110 }
111
112 if let Some(seed) = config_seed.as_ref().filter(|s| !s.trim().is_empty()) {
113 return Some(seed.clone());
114 }
115
116 None
117}
118
119pub fn global_config_display_path(ctx: &ConfigContext) -> String {
125 global_config_path(ctx)
126 .map(|p| p.display().to_string())
127 .unwrap_or_else(|| "~/.config/ito/config.json".to_string())
128}
129
130pub fn write_auth_to_global_config(
136 ctx: &ConfigContext,
137 admin_token: &str,
138 token_seed: &str,
139) -> CoreResult<()> {
140 let Some(config_path) = global_config_path(ctx) else {
141 return Err(CoreError::validation(
142 "Cannot determine global config path (HOME not set)",
143 ));
144 };
145
146 if let Some(parent) = config_path.parent() {
148 std::fs::create_dir_all(parent)
149 .map_err(|e| CoreError::io(format!("create config dir {}", parent.display()), e))?;
150
151 #[cfg(unix)]
152 {
153 use std::os::unix::fs::PermissionsExt;
154 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).map_err(
155 |e| CoreError::io(format!("set permissions on {}", parent.display()), e),
156 )?;
157 }
158 }
159
160 let mut doc: serde_json::Value = if config_path.exists() {
162 let contents = std::fs::read_to_string(&config_path)
163 .map_err(|e| CoreError::io(format!("read {}", config_path.display()), e))?;
164 serde_json::from_str(&contents).unwrap_or_else(|_| serde_json::json!({}))
165 } else {
166 serde_json::json!({})
167 };
168
169 let root = doc.as_object_mut().ok_or_else(|| {
171 CoreError::validation(format!(
172 "{} is not a JSON object; delete it and re-run --init",
173 config_path.display()
174 ))
175 })?;
176
177 let backend_server = root
178 .entry("backendServer")
179 .or_insert_with(|| serde_json::json!({}))
180 .as_object_mut()
181 .ok_or_else(|| CoreError::validation("config key 'backendServer' must be a JSON object"))?;
182
183 let auth_obj = backend_server
184 .entry("auth")
185 .or_insert_with(|| serde_json::json!({}))
186 .as_object_mut()
187 .ok_or_else(|| {
188 CoreError::validation("config key 'backendServer.auth' must be a JSON object")
189 })?;
190
191 auth_obj.insert("adminTokens".to_string(), serde_json::json!([admin_token]));
192 auth_obj.insert("tokenSeed".to_string(), serde_json::json!(token_seed));
193
194 let formatted = serde_json::to_string_pretty(&doc)
196 .map_err(|e| CoreError::serde("serialize global config", e.to_string()))?;
197
198 write_config_file(&config_path, &(formatted + "\n"))?;
199
200 Ok(())
201}
202
203fn write_config_file(path: &Path, content: &str) -> CoreResult<()> {
208 #[cfg(unix)]
209 {
210 use std::io::Write;
211 use std::os::unix::fs::OpenOptionsExt;
212
213 let mut file = std::fs::OpenOptions::new()
214 .write(true)
215 .create(true)
216 .truncate(true)
217 .mode(0o600)
218 .open(path)
219 .map_err(|e| CoreError::io(format!("write {}", path.display()), e))?;
220
221 file.write_all(content.as_bytes())
222 .map_err(|e| CoreError::io(format!("write {}", path.display()), e))?;
223 }
224
225 #[cfg(not(unix))]
226 {
227 std::fs::write(path, content)
228 .map_err(|e| CoreError::io(format!("write {}", path.display()), e))?;
229 }
230
231 Ok(())
232}