cascade_cli/config/
auth.rs1use crate::errors::{CascadeError, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7pub struct AuthConfig {
8 pub bitbucket_tokens: std::collections::HashMap<String, String>,
9 pub default_server: Option<String>,
10}
11
12pub struct AuthManager {
13 config: AuthConfig,
14 config_path: std::path::PathBuf,
15}
16
17impl AuthManager {
18 pub fn new(config_dir: &Path) -> Result<Self> {
20 let config_path = config_dir.join("auth.json");
21 let config = if config_path.exists() {
22 AuthConfig::load_from_file(&config_path)?
23 } else {
24 AuthConfig::default()
25 };
26
27 Ok(Self {
28 config,
29 config_path,
30 })
31 }
32
33 pub fn store_token(&mut self, server_url: &str, token: &str) -> Result<()> {
35 self.config
36 .bitbucket_tokens
37 .insert(server_url.to_string(), token.to_string());
38 self.save()?;
39 tracing::debug!("Stored authentication token for {}", server_url);
40 Ok(())
41 }
42
43 pub fn get_token(&self, server_url: &str) -> Option<&String> {
45 self.config.bitbucket_tokens.get(server_url)
46 }
47
48 pub fn remove_token(&mut self, server_url: &str) -> Result<bool> {
50 let removed = self.config.bitbucket_tokens.remove(server_url).is_some();
51 if removed {
52 self.save()?;
53 tracing::debug!("Removed authentication token for {}", server_url);
54 }
55 Ok(removed)
56 }
57
58 pub fn list_servers(&self) -> Vec<&String> {
60 self.config.bitbucket_tokens.keys().collect()
61 }
62
63 pub fn set_default_server(&mut self, server_url: &str) -> Result<()> {
65 if !self.config.bitbucket_tokens.contains_key(server_url) {
66 return Err(CascadeError::auth(format!(
67 "No token configured for server: {server_url}"
68 )));
69 }
70
71 self.config.default_server = Some(server_url.to_string());
72 self.save()?;
73 tracing::debug!("Set default server to {}", server_url);
74 Ok(())
75 }
76
77 pub fn get_default_server(&self) -> Option<&String> {
79 self.config.default_server.as_ref()
80 }
81
82 pub fn validate_auth(&self, server_url: &str) -> Result<()> {
84 if self.get_token(server_url).is_none() {
85 return Err(CascadeError::auth(format!(
86 "No authentication token configured for server: {server_url}. Use 'ca config set bitbucket.token <token>' to configure."
87 )));
88 }
89 Ok(())
90 }
91
92 fn save(&self) -> Result<()> {
94 self.config.save_to_file(&self.config_path)
95 }
96}
97
98impl AuthConfig {
99 pub fn load_from_file(path: &Path) -> Result<Self> {
101 if !path.exists() {
102 return Ok(Self::default());
103 }
104
105 let content = fs::read_to_string(path)
106 .map_err(|e| CascadeError::config(format!("Failed to read auth config: {e}")))?;
107
108 let config: AuthConfig = serde_json::from_str(&content)
109 .map_err(|e| CascadeError::config(format!("Failed to parse auth config: {e}")))?;
110
111 Ok(config)
112 }
113
114 pub fn save_to_file(&self, path: &Path) -> Result<()> {
116 if let Some(parent) = path.parent() {
118 fs::create_dir_all(parent).map_err(|e| {
119 CascadeError::config(format!("Failed to create config directory: {e}"))
120 })?;
121 }
122
123 let content = serde_json::to_string_pretty(self)
124 .map_err(|e| CascadeError::config(format!("Failed to serialize auth config: {e}")))?;
125
126 let temp_path = path.with_extension("tmp");
128 fs::write(&temp_path, content)
129 .map_err(|e| CascadeError::config(format!("Failed to write auth config: {e}")))?;
130
131 fs::rename(&temp_path, path)
132 .map_err(|e| CascadeError::config(format!("Failed to finalize auth config: {e}")))?;
133
134 Ok(())
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use tempfile::TempDir;
142
143 #[test]
144 fn test_auth_manager_basic_operations() {
145 let temp_dir = TempDir::new().unwrap();
146 let config_dir = temp_dir.path();
147
148 let mut auth_manager = AuthManager::new(config_dir).unwrap();
149
150 auth_manager
152 .store_token("https://bitbucket.company.com", "test-token")
153 .unwrap();
154 assert_eq!(
155 auth_manager.get_token("https://bitbucket.company.com"),
156 Some(&"test-token".to_string())
157 );
158
159 auth_manager
161 .set_default_server("https://bitbucket.company.com")
162 .unwrap();
163 assert_eq!(
164 auth_manager.get_default_server(),
165 Some(&"https://bitbucket.company.com".to_string())
166 );
167
168 auth_manager
170 .validate_auth("https://bitbucket.company.com")
171 .unwrap();
172 assert!(auth_manager
173 .validate_auth("https://unknown.server.com")
174 .is_err());
175
176 assert!(auth_manager
178 .remove_token("https://bitbucket.company.com")
179 .unwrap());
180 assert!(!auth_manager
181 .remove_token("https://bitbucket.company.com")
182 .unwrap());
183 }
184}