Skip to main content

rusty_commit/config/
accounts.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6
7/// Authentication method for an account
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(tag = "type")]
10pub enum AuthMethod {
11    #[serde(rename = "api_key")]
12    ApiKey { key_id: String },
13    #[serde(rename = "oauth")]
14    OAuth {
15        provider: String,
16        account_id: String,
17    },
18    #[serde(rename = "env_var")]
19    EnvVar { name: String },
20    #[serde(rename = "bearer")]
21    Bearer { token_id: String },
22}
23
24/// Single account configuration
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AccountConfig {
27    pub alias: String,
28    pub provider: String,
29    pub api_url: Option<String>,
30    pub model: Option<String>,
31    pub auth: AuthMethod,
32    pub tokens_max_input: Option<usize>,
33    pub tokens_max_output: Option<u32>,
34    #[serde(default)]
35    pub is_default: bool,
36}
37
38/// All accounts configuration
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct AccountsConfig {
41    pub active_account: Option<String>,
42    pub accounts: HashMap<String, AccountConfig>,
43}
44
45#[allow(dead_code)]
46impl AccountsConfig {
47    /// Get the path to the accounts file
48    fn accounts_file_path() -> Result<PathBuf> {
49        let config_dir = if let Ok(config_home) = std::env::var("RCO_CONFIG_HOME") {
50            PathBuf::from(config_home)
51        } else {
52            let home = dirs::home_dir().context("Could not find home directory")?;
53            home.join(".config").join("rustycommit")
54        };
55
56        // Ensure directory exists
57        if !config_dir.exists() {
58            fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
59        }
60
61        Ok(config_dir.join("accounts.toml"))
62    }
63
64    /// Load accounts from file
65    pub fn load() -> Result<Option<Self>> {
66        let path = Self::accounts_file_path()?;
67
68        if !path.exists() {
69            return Ok(None);
70        }
71
72        let contents = fs::read_to_string(&path).context("Failed to read accounts file")?;
73
74        let config: AccountsConfig =
75            toml::from_str(&contents).context("Failed to parse accounts file")?;
76
77        Ok(Some(config))
78    }
79
80    /// Save accounts to file
81    pub fn save(&self) -> Result<()> {
82        let path = Self::accounts_file_path()?;
83
84        let toml_content = toml::to_string_pretty(self).context("Failed to serialize accounts")?;
85
86        fs::write(&path, toml_content).context("Failed to write accounts file")?;
87
88        // Set file permissions to 600 (user read/write only) on Unix
89        #[cfg(unix)]
90        {
91            use std::os::unix::fs::PermissionsExt;
92            let mut perms = fs::metadata(&path)?.permissions();
93            perms.set_mode(0o600);
94            fs::set_permissions(&path, perms).context("Failed to set accounts file permissions")?;
95        }
96
97        Ok(())
98    }
99
100    /// Get an account by alias
101    pub fn get_account(&self, alias: &str) -> Option<&AccountConfig> {
102        self.accounts.get(alias)
103    }
104
105    /// Get an account by alias (mutable)
106    pub fn get_account_mut(&mut self, alias: &str) -> Option<&mut AccountConfig> {
107        self.accounts.get_mut(alias)
108    }
109
110    /// Add or update an account
111    pub fn add_account(&mut self, account: AccountConfig) {
112        self.accounts.insert(account.alias.clone(), account);
113    }
114
115    /// Remove an account
116    pub fn remove_account(&mut self, alias: &str) -> bool {
117        self.accounts.remove(alias).is_some()
118    }
119
120    /// List all accounts
121    pub fn list_accounts(&self) -> Vec<&AccountConfig> {
122        self.accounts.values().collect()
123    }
124
125    /// Set an account as active
126    pub fn set_active_account(&mut self, alias: &str) -> Result<()> {
127        if !self.accounts.contains_key(alias) {
128            anyhow::bail!("Account '{}' not found", alias);
129        }
130        self.active_account = Some(alias.to_string());
131        Ok(())
132    }
133
134    /// Get the active account
135    pub fn get_active_account(&self) -> Option<&AccountConfig> {
136        if let Some(alias) = &self.active_account {
137            self.accounts.get(alias)
138        } else {
139            None
140        }
141    }
142
143    /// Get active account alias
144    pub fn get_active_alias(&self) -> Option<&str> {
145        self.active_account.as_deref()
146    }
147
148    /// Get a unique key ID for an account and auth method
149    pub fn get_key_id(account_alias: &str, auth_type: &str) -> String {
150        format!("rco_{}_{}", account_alias, auth_type)
151    }
152}
153
154/// Get the secure storage key prefix for an account
155#[allow(dead_code)]
156pub fn account_storage_key(account_alias: &str, key_type: &str) -> String {
157    format!("rco_account_{}_{}", account_alias, key_type)
158}
159
160/// Delete all storage keys for an account
161#[allow(dead_code)]
162pub fn delete_account_storage(_account_alias: &str) {
163    #[cfg(feature = "secure-storage")]
164    {
165        use crate::config::secure_storage;
166
167        for key_type in ["access_token", "refresh_token", "api_key", "bearer_token"] {
168            let key = account_storage_key(_account_alias, key_type);
169            let _ = secure_storage::delete_secret(&key);
170        }
171    }
172    // For file-based storage, keys are stored in the accounts.toml itself
173    // API keys in auth method are encrypted/obfuscated if needed
174}