garmin_cli/config/
credentials.rs1use crate::client::{OAuth1Token, OAuth2Token};
2use crate::error::{GarminError, Result};
3use std::fs;
4use std::path::PathBuf;
5
6const OAUTH1_FILENAME: &str = "oauth1_token.json";
7const OAUTH2_FILENAME: &str = "oauth2_token.json";
8const SERVICE_NAME: &str = "garmin-cli";
9
10pub struct CredentialStore {
13 profile: String,
14 base_dir: PathBuf,
15}
16
17impl CredentialStore {
18 pub fn new(profile: Option<String>) -> Result<Self> {
20 let profile = profile.unwrap_or_else(|| "default".to_string());
21 let base_dir = super::data_dir()?.join(&profile);
22 super::ensure_dir(&base_dir)?;
23
24 Ok(Self { profile, base_dir })
25 }
26
27 pub fn with_dir(profile: impl Into<String>, base_dir: PathBuf) -> Result<Self> {
29 let profile = profile.into();
30 let dir = base_dir.join(&profile);
31 super::ensure_dir(&dir)?;
32
33 Ok(Self {
34 profile,
35 base_dir: dir,
36 })
37 }
38
39 pub fn profile(&self) -> &str {
41 &self.profile
42 }
43
44 pub fn save_oauth1(&self, token: &OAuth1Token) -> Result<()> {
46 let path = self.base_dir.join(OAUTH1_FILENAME);
47 let json = serde_json::to_string_pretty(token)?;
48 fs::write(&path, json)?;
49
50 #[cfg(unix)]
52 {
53 use std::os::unix::fs::PermissionsExt;
54 fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
55 }
56
57 Ok(())
58 }
59
60 pub fn load_oauth1(&self) -> Result<Option<OAuth1Token>> {
62 let path = self.base_dir.join(OAUTH1_FILENAME);
63 if !path.exists() {
64 return Ok(None);
65 }
66
67 let json = fs::read_to_string(&path)?;
68 let token: OAuth1Token = serde_json::from_str(&json)?;
69 Ok(Some(token))
70 }
71
72 pub fn save_oauth2(&self, token: &OAuth2Token) -> Result<()> {
74 let path = self.base_dir.join(OAUTH2_FILENAME);
75 let json = serde_json::to_string_pretty(token)?;
76 fs::write(&path, json)?;
77
78 #[cfg(unix)]
80 {
81 use std::os::unix::fs::PermissionsExt;
82 fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
83 }
84
85 Ok(())
86 }
87
88 pub fn load_oauth2(&self) -> Result<Option<OAuth2Token>> {
90 let path = self.base_dir.join(OAUTH2_FILENAME);
91 if !path.exists() {
92 return Ok(None);
93 }
94
95 let json = fs::read_to_string(&path)?;
96 let token: OAuth2Token = serde_json::from_str(&json)?;
97 Ok(Some(token))
98 }
99
100 pub fn save_tokens(&self, oauth1: &OAuth1Token, oauth2: &OAuth2Token) -> Result<()> {
102 self.save_oauth1(oauth1)?;
103 self.save_oauth2(oauth2)?;
104 Ok(())
105 }
106
107 pub fn load_tokens(&self) -> Result<Option<(OAuth1Token, OAuth2Token)>> {
109 let oauth1 = self.load_oauth1()?;
110 let oauth2 = self.load_oauth2()?;
111
112 match (oauth1, oauth2) {
113 (Some(o1), Some(o2)) => Ok(Some((o1, o2))),
114 _ => Ok(None),
115 }
116 }
117
118 pub fn has_credentials(&self) -> bool {
120 self.base_dir.join(OAUTH1_FILENAME).exists() && self.base_dir.join(OAUTH2_FILENAME).exists()
121 }
122
123 pub fn clear(&self) -> Result<()> {
125 let oauth1_path = self.base_dir.join(OAUTH1_FILENAME);
126 let oauth2_path = self.base_dir.join(OAUTH2_FILENAME);
127
128 if oauth1_path.exists() {
129 fs::remove_file(oauth1_path)?;
130 }
131 if oauth2_path.exists() {
132 fs::remove_file(oauth2_path)?;
133 }
134
135 Ok(())
136 }
137
138 pub fn store_secret_in_keyring(&self, secret: &str) -> Result<()> {
141 let entry = keyring::Entry::new(SERVICE_NAME, &self.profile)
142 .map_err(|e| GarminError::Keyring(e.to_string()))?;
143
144 entry
145 .set_password(secret)
146 .map_err(|e| GarminError::Keyring(e.to_string()))?;
147
148 Ok(())
149 }
150
151 pub fn load_secret_from_keyring(&self) -> Result<Option<String>> {
153 let entry = keyring::Entry::new(SERVICE_NAME, &self.profile)
154 .map_err(|e| GarminError::Keyring(e.to_string()))?;
155
156 match entry.get_password() {
157 Ok(secret) => Ok(Some(secret)),
158 Err(keyring::Error::NoEntry) => Ok(None),
159 Err(e) => Err(GarminError::Keyring(e.to_string())),
160 }
161 }
162
163 pub fn delete_secret_from_keyring(&self) -> Result<()> {
165 let entry = keyring::Entry::new(SERVICE_NAME, &self.profile)
166 .map_err(|e| GarminError::Keyring(e.to_string()))?;
167
168 match entry.delete_credential() {
169 Ok(()) => Ok(()),
170 Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(GarminError::Keyring(e.to_string())),
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use chrono::Utc;
180 use tempfile::TempDir;
181
182 fn create_test_oauth1() -> OAuth1Token {
183 OAuth1Token::new("test_token".to_string(), "test_secret".to_string())
184 }
185
186 fn create_test_oauth2() -> OAuth2Token {
187 OAuth2Token {
188 scope: "test_scope".to_string(),
189 jti: "test_jti".to_string(),
190 token_type: "Bearer".to_string(),
191 access_token: "test_access".to_string(),
192 refresh_token: "test_refresh".to_string(),
193 expires_in: 3600,
194 expires_at: Utc::now().timestamp() + 3600,
195 refresh_token_expires_in: 86400,
196 refresh_token_expires_at: Utc::now().timestamp() + 86400,
197 }
198 }
199
200 #[test]
201 fn test_credential_store_creation() {
202 let temp_dir = TempDir::new().unwrap();
203 let store = CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf());
204 assert!(store.is_ok());
205 assert_eq!(store.unwrap().profile(), "test_profile");
206 }
207
208 #[test]
209 fn test_save_and_load_oauth1() {
210 let temp_dir = TempDir::new().unwrap();
211 let store =
212 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
213
214 let token = create_test_oauth1();
215 store.save_oauth1(&token).unwrap();
216
217 let loaded = store.load_oauth1().unwrap();
218 assert!(loaded.is_some());
219 assert_eq!(loaded.unwrap(), token);
220 }
221
222 #[test]
223 fn test_save_and_load_oauth2() {
224 let temp_dir = TempDir::new().unwrap();
225 let store =
226 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
227
228 let token = create_test_oauth2();
229 store.save_oauth2(&token).unwrap();
230
231 let loaded = store.load_oauth2().unwrap();
232 assert!(loaded.is_some());
233 let loaded_token = loaded.unwrap();
235 assert_eq!(loaded_token.access_token, token.access_token);
236 assert_eq!(loaded_token.refresh_token, token.refresh_token);
237 }
238
239 #[test]
240 fn test_load_missing_oauth1() {
241 let temp_dir = TempDir::new().unwrap();
242 let store =
243 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
244
245 let loaded = store.load_oauth1().unwrap();
246 assert!(loaded.is_none());
247 }
248
249 #[test]
250 fn test_load_missing_oauth2() {
251 let temp_dir = TempDir::new().unwrap();
252 let store =
253 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
254
255 let loaded = store.load_oauth2().unwrap();
256 assert!(loaded.is_none());
257 }
258
259 #[test]
260 fn test_save_and_load_both_tokens() {
261 let temp_dir = TempDir::new().unwrap();
262 let store =
263 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
264
265 let oauth1 = create_test_oauth1();
266 let oauth2 = create_test_oauth2();
267 store.save_tokens(&oauth1, &oauth2).unwrap();
268
269 let loaded = store.load_tokens().unwrap();
270 assert!(loaded.is_some());
271 let (loaded_oauth1, loaded_oauth2) = loaded.unwrap();
272 assert_eq!(loaded_oauth1, oauth1);
273 assert_eq!(loaded_oauth2.access_token, oauth2.access_token);
274 }
275
276 #[test]
277 fn test_has_credentials() {
278 let temp_dir = TempDir::new().unwrap();
279 let store =
280 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
281
282 assert!(!store.has_credentials());
283
284 let oauth1 = create_test_oauth1();
285 let oauth2 = create_test_oauth2();
286 store.save_tokens(&oauth1, &oauth2).unwrap();
287
288 assert!(store.has_credentials());
289 }
290
291 #[test]
292 fn test_clear_credentials() {
293 let temp_dir = TempDir::new().unwrap();
294 let store =
295 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
296
297 let oauth1 = create_test_oauth1();
298 let oauth2 = create_test_oauth2();
299 store.save_tokens(&oauth1, &oauth2).unwrap();
300 assert!(store.has_credentials());
301
302 store.clear().unwrap();
303 assert!(!store.has_credentials());
304 }
305
306 #[test]
307 fn test_partial_tokens_returns_none() {
308 let temp_dir = TempDir::new().unwrap();
309 let store =
310 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
311
312 let oauth1 = create_test_oauth1();
314 store.save_oauth1(&oauth1).unwrap();
315
316 let loaded = store.load_tokens().unwrap();
318 assert!(loaded.is_none());
319 }
320}