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()
121 && self.base_dir.join(OAUTH2_FILENAME).exists()
122 }
123
124 pub fn clear(&self) -> Result<()> {
126 let oauth1_path = self.base_dir.join(OAUTH1_FILENAME);
127 let oauth2_path = self.base_dir.join(OAUTH2_FILENAME);
128
129 if oauth1_path.exists() {
130 fs::remove_file(oauth1_path)?;
131 }
132 if oauth2_path.exists() {
133 fs::remove_file(oauth2_path)?;
134 }
135
136 Ok(())
137 }
138
139 pub fn store_secret_in_keyring(&self, secret: &str) -> Result<()> {
142 let entry = keyring::Entry::new(SERVICE_NAME, &self.profile)
143 .map_err(|e| GarminError::Keyring(e.to_string()))?;
144
145 entry
146 .set_password(secret)
147 .map_err(|e| GarminError::Keyring(e.to_string()))?;
148
149 Ok(())
150 }
151
152 pub fn load_secret_from_keyring(&self) -> Result<Option<String>> {
154 let entry = keyring::Entry::new(SERVICE_NAME, &self.profile)
155 .map_err(|e| GarminError::Keyring(e.to_string()))?;
156
157 match entry.get_password() {
158 Ok(secret) => Ok(Some(secret)),
159 Err(keyring::Error::NoEntry) => Ok(None),
160 Err(e) => Err(GarminError::Keyring(e.to_string())),
161 }
162 }
163
164 pub fn delete_secret_from_keyring(&self) -> Result<()> {
166 let entry = keyring::Entry::new(SERVICE_NAME, &self.profile)
167 .map_err(|e| GarminError::Keyring(e.to_string()))?;
168
169 match entry.delete_credential() {
170 Ok(()) => Ok(()),
171 Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(GarminError::Keyring(e.to_string())),
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use chrono::Utc;
181 use tempfile::TempDir;
182
183 fn create_test_oauth1() -> OAuth1Token {
184 OAuth1Token::new("test_token".to_string(), "test_secret".to_string())
185 }
186
187 fn create_test_oauth2() -> OAuth2Token {
188 OAuth2Token {
189 scope: "test_scope".to_string(),
190 jti: "test_jti".to_string(),
191 token_type: "Bearer".to_string(),
192 access_token: "test_access".to_string(),
193 refresh_token: "test_refresh".to_string(),
194 expires_in: 3600,
195 expires_at: Utc::now().timestamp() + 3600,
196 refresh_token_expires_in: 86400,
197 refresh_token_expires_at: Utc::now().timestamp() + 86400,
198 }
199 }
200
201 #[test]
202 fn test_credential_store_creation() {
203 let temp_dir = TempDir::new().unwrap();
204 let store = CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf());
205 assert!(store.is_ok());
206 assert_eq!(store.unwrap().profile(), "test_profile");
207 }
208
209 #[test]
210 fn test_save_and_load_oauth1() {
211 let temp_dir = TempDir::new().unwrap();
212 let store =
213 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
214
215 let token = create_test_oauth1();
216 store.save_oauth1(&token).unwrap();
217
218 let loaded = store.load_oauth1().unwrap();
219 assert!(loaded.is_some());
220 assert_eq!(loaded.unwrap(), token);
221 }
222
223 #[test]
224 fn test_save_and_load_oauth2() {
225 let temp_dir = TempDir::new().unwrap();
226 let store =
227 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
228
229 let token = create_test_oauth2();
230 store.save_oauth2(&token).unwrap();
231
232 let loaded = store.load_oauth2().unwrap();
233 assert!(loaded.is_some());
234 let loaded_token = loaded.unwrap();
236 assert_eq!(loaded_token.access_token, token.access_token);
237 assert_eq!(loaded_token.refresh_token, token.refresh_token);
238 }
239
240 #[test]
241 fn test_load_missing_oauth1() {
242 let temp_dir = TempDir::new().unwrap();
243 let store =
244 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
245
246 let loaded = store.load_oauth1().unwrap();
247 assert!(loaded.is_none());
248 }
249
250 #[test]
251 fn test_load_missing_oauth2() {
252 let temp_dir = TempDir::new().unwrap();
253 let store =
254 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
255
256 let loaded = store.load_oauth2().unwrap();
257 assert!(loaded.is_none());
258 }
259
260 #[test]
261 fn test_save_and_load_both_tokens() {
262 let temp_dir = TempDir::new().unwrap();
263 let store =
264 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
265
266 let oauth1 = create_test_oauth1();
267 let oauth2 = create_test_oauth2();
268 store.save_tokens(&oauth1, &oauth2).unwrap();
269
270 let loaded = store.load_tokens().unwrap();
271 assert!(loaded.is_some());
272 let (loaded_oauth1, loaded_oauth2) = loaded.unwrap();
273 assert_eq!(loaded_oauth1, oauth1);
274 assert_eq!(loaded_oauth2.access_token, oauth2.access_token);
275 }
276
277 #[test]
278 fn test_has_credentials() {
279 let temp_dir = TempDir::new().unwrap();
280 let store =
281 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
282
283 assert!(!store.has_credentials());
284
285 let oauth1 = create_test_oauth1();
286 let oauth2 = create_test_oauth2();
287 store.save_tokens(&oauth1, &oauth2).unwrap();
288
289 assert!(store.has_credentials());
290 }
291
292 #[test]
293 fn test_clear_credentials() {
294 let temp_dir = TempDir::new().unwrap();
295 let store =
296 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
297
298 let oauth1 = create_test_oauth1();
299 let oauth2 = create_test_oauth2();
300 store.save_tokens(&oauth1, &oauth2).unwrap();
301 assert!(store.has_credentials());
302
303 store.clear().unwrap();
304 assert!(!store.has_credentials());
305 }
306
307 #[test]
308 fn test_partial_tokens_returns_none() {
309 let temp_dir = TempDir::new().unwrap();
310 let store =
311 CredentialStore::with_dir("test_profile", temp_dir.path().to_path_buf()).unwrap();
312
313 let oauth1 = create_test_oauth1();
315 store.save_oauth1(&oauth1).unwrap();
316
317 let loaded = store.load_tokens().unwrap();
319 assert!(loaded.is_none());
320 }
321}