rusty_commit/auth/
token_storage.rs1use anyhow::{Context, Result};
2use dirs::home_dir;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6
7fn current_unix_timestamp() -> Option<u64> {
11 std::time::SystemTime::now()
12 .duration_since(std::time::UNIX_EPOCH)
13 .ok()
14 .map(|d| d.as_secs())
15}
16
17#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct TokenStorage {
20 pub access_token: String,
21 pub refresh_token: Option<String>,
22 pub expires_at: Option<u64>,
23 pub token_type: String,
24 pub scope: Option<String>,
25}
26
27impl TokenStorage {
28 fn auth_file_path() -> Result<PathBuf> {
30 let config_dir = if let Ok(config_home) = std::env::var("RCO_CONFIG_HOME") {
31 PathBuf::from(config_home)
32 } else {
33 let home = home_dir().context("Could not find home directory")?;
34 home.join(".config").join("rustycommit")
35 };
36
37 if !config_dir.exists() {
39 fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
40 }
41
42 Ok(config_dir.join("auth.json"))
43 }
44
45 pub fn save(&self) -> Result<()> {
47 let path = Self::auth_file_path()?;
48
49 let json = serde_json::to_string_pretty(self).context("Failed to serialize token data")?;
51
52 fs::write(&path, json).context("Failed to write auth token file")?;
54
55 #[cfg(unix)]
57 {
58 use std::os::unix::fs::PermissionsExt;
59 let mut perms = fs::metadata(&path)?.permissions();
60 perms.set_mode(0o600);
61 fs::set_permissions(&path, perms).context("Failed to set auth file permissions")?;
62 }
63
64 Ok(())
65 }
66
67 pub fn load() -> Result<Option<Self>> {
69 let path = Self::auth_file_path()?;
70
71 if !path.exists() {
72 return Ok(None);
73 }
74
75 let contents = fs::read_to_string(&path).context("Failed to read auth token file")?;
76
77 let storage: TokenStorage =
78 serde_json::from_str(&contents).context("Failed to parse auth token file")?;
79
80 Ok(Some(storage))
81 }
82
83 pub fn delete() -> Result<()> {
85 let path = Self::auth_file_path()?;
86
87 if path.exists() {
88 fs::remove_file(&path).context("Failed to delete auth token file")?;
89 }
90
91 Ok(())
92 }
93
94 pub fn is_expired(&self) -> bool {
96 if let Some(expires_at) = self.expires_at {
97 let now = current_unix_timestamp().unwrap_or(u64::MAX);
98 now >= expires_at
99 } else {
100 false
101 }
102 }
103
104 #[allow(dead_code)]
106 pub fn expires_soon(&self) -> bool {
107 if let Some(expires_at) = self.expires_at {
108 let now = current_unix_timestamp().unwrap_or(u64::MAX);
109 now >= expires_at.saturating_sub(300) } else {
111 false
112 }
113 }
114}
115
116pub fn store_tokens(
118 access_token: &str,
119 refresh_token: Option<&str>,
120 expires_in: Option<u64>,
121) -> Result<()> {
122 #[cfg(feature = "secure-storage")]
124 {
125 if crate::config::secure_storage::is_available() {
126 if let Err(e) =
128 crate::config::secure_storage::store_secret("claude_access_token", access_token)
129 {
130 eprintln!(
131 "Note: Could not store access token in secure storage: {}",
132 e
133 );
134 } else {
135 if let Some(refresh) = refresh_token {
136 if let Err(e) =
137 crate::config::secure_storage::store_secret("claude_refresh_token", refresh)
138 {
139 eprintln!(
140 "Note: Could not store refresh token in secure storage: {}",
141 e
142 );
143 }
144 }
145
146 if let Some(expires_in) = expires_in {
147 let expires_at = current_unix_timestamp().unwrap_or(u64::MAX) + expires_in;
148 if let Err(e) = crate::config::secure_storage::store_secret(
149 "claude_token_expires_at",
150 &expires_at.to_string(),
151 ) {
152 eprintln!(
153 "Note: Could not store token expiry in secure storage: {}",
154 e
155 );
156 }
157 }
158
159 return Ok(());
161 }
162 }
163 }
164
165 let expires_at = expires_in.map(|exp| current_unix_timestamp().unwrap_or(u64::MAX) + exp);
167
168 let storage = TokenStorage {
169 access_token: access_token.to_string(),
170 refresh_token: refresh_token.map(|s| s.to_string()),
171 expires_at,
172 token_type: "Bearer".to_string(),
173 scope: Some("openid profile email".to_string()),
174 };
175
176 storage.save()?;
177 Ok(())
178}
179
180pub fn get_tokens() -> Result<Option<TokenStorage>> {
182 #[cfg(feature = "secure-storage")]
184 {
185 if crate::config::secure_storage::is_available() {
186 if let Ok(Some(access_token)) =
187 crate::config::secure_storage::get_secret("claude_access_token")
188 {
189 let refresh_token =
190 crate::config::secure_storage::get_secret("claude_refresh_token")
191 .ok()
192 .flatten();
193
194 let expires_at =
195 crate::config::secure_storage::get_secret("claude_token_expires_at")
196 .ok()
197 .flatten()
198 .and_then(|s| s.parse::<u64>().ok());
199
200 return Ok(Some(TokenStorage {
201 access_token,
202 refresh_token,
203 expires_at,
204 token_type: "Bearer".to_string(),
205 scope: Some("openid profile email".to_string()),
206 }));
207 }
208 }
209 }
210
211 TokenStorage::load()
213}
214
215pub fn delete_tokens() -> Result<()> {
217 #[cfg(feature = "secure-storage")]
219 {
220 let _ = crate::config::secure_storage::delete_secret("claude_access_token");
221 let _ = crate::config::secure_storage::delete_secret("claude_refresh_token");
222 let _ = crate::config::secure_storage::delete_secret("claude_token_expires_at");
223 }
224
225 TokenStorage::delete()?;
227 Ok(())
228}
229
230pub fn get_access_token() -> Result<Option<String>> {
232 Ok(get_tokens()?.map(|t| t.access_token))
233}
234
235pub fn has_valid_token() -> bool {
237 if let Ok(Some(tokens)) = get_tokens() {
238 !tokens.is_expired()
239 } else {
240 false
241 }
242}
243
244#[allow(dead_code)]
250fn account_storage_key(account_id: &str, key_type: &str) -> String {
251 format!("rco_account_{}_{}", account_id, key_type)
252}
253
254#[allow(dead_code)]
256pub fn store_tokens_for_account(
257 _account_id: &str,
258 access_token: &str,
259 refresh_token: Option<&str>,
260 expires_in: Option<u64>,
261) -> Result<()> {
262 #[cfg(feature = "secure-storage")]
264 {
265 if crate::config::secure_storage::is_available() {
266 let access_key = account_storage_key(_account_id, "access_token");
267 if let Err(e) = crate::config::secure_storage::store_secret(&access_key, access_token) {
268 eprintln!(
269 "Note: Could not store access token in secure storage: {}",
270 e
271 );
272 } else {
273 if let Some(refresh) = refresh_token {
275 let refresh_key = account_storage_key(_account_id, "refresh_token");
276 let _ = crate::config::secure_storage::store_secret(&refresh_key, refresh);
277 }
278
279 if let Some(expires_in) = expires_in {
281 let expires_at = current_unix_timestamp().unwrap_or(u64::MAX) + expires_in;
282 let expiry_key = account_storage_key(_account_id, "token_expires_at");
283 let _ = crate::config::secure_storage::store_secret(
284 &expiry_key,
285 &expires_at.to_string(),
286 );
287 }
288
289 return Ok(());
290 }
291 }
292 }
293
294 store_tokens(access_token, refresh_token, expires_in)
297}
298
299#[allow(dead_code)]
301pub fn get_tokens_for_account(_account_id: &str) -> Result<Option<TokenStorage>> {
302 #[cfg(feature = "secure-storage")]
304 {
305 if crate::config::secure_storage::is_available() {
306 let access_key = account_storage_key(_account_id, "access_token");
307 if let Ok(Some(access_token)) = crate::config::secure_storage::get_secret(&access_key) {
308 let refresh_key = account_storage_key(_account_id, "refresh_token");
309 let refresh_token = crate::config::secure_storage::get_secret(&refresh_key)
310 .ok()
311 .flatten();
312
313 let expiry_key = account_storage_key(_account_id, "token_expires_at");
314 let expires_at = crate::config::secure_storage::get_secret(&expiry_key)
315 .ok()
316 .flatten()
317 .and_then(|s| s.parse::<u64>().ok());
318
319 return Ok(Some(TokenStorage {
320 access_token,
321 refresh_token,
322 expires_at,
323 token_type: "Bearer".to_string(),
324 scope: Some("openid profile email".to_string()),
325 }));
326 }
327 }
328 }
329
330 get_tokens()
332}
333
334#[allow(dead_code)]
336pub fn delete_tokens_for_account(_account_id: &str) -> Result<()> {
337 #[cfg(feature = "secure-storage")]
338 {
339 if crate::config::secure_storage::is_available() {
340 for key_type in ["access_token", "refresh_token", "token_expires_at"] {
341 let key = account_storage_key(_account_id, key_type);
342 let _ = crate::config::secure_storage::delete_secret(&key);
343 }
344 }
345 }
346
347 Ok(())
348}
349
350#[allow(dead_code)]
352pub fn store_api_key_for_account(_account_id: &str, _api_key: &str) -> Result<()> {
353 #[cfg(feature = "secure-storage")]
354 {
355 if crate::config::secure_storage::is_available() {
356 let key = account_storage_key(_account_id, "api_key");
357 crate::config::secure_storage::store_secret(&key, _api_key)?;
358 return Ok(());
359 }
360 }
361
362 anyhow::bail!(
364 "Secure storage not available. Cannot store API key for account '{}'.",
365 _account_id
366 )
367}
368
369#[allow(dead_code)]
371pub fn get_api_key_for_account(_account_id: &str) -> Result<Option<String>> {
372 #[cfg(feature = "secure-storage")]
373 {
374 if crate::config::secure_storage::is_available() {
375 let key = account_storage_key(_account_id, "api_key");
376 return crate::config::secure_storage::get_secret(&key);
377 }
378 }
379
380 Ok(None)
381}
382
383#[allow(dead_code)]
385pub fn store_bearer_token_for_account(_account_id: &str, _token: &str) -> Result<()> {
386 #[cfg(feature = "secure-storage")]
387 {
388 if crate::config::secure_storage::is_available() {
389 let key = account_storage_key(_account_id, "bearer_token");
390 crate::config::secure_storage::store_secret(&key, _token)?;
391 return Ok(());
392 }
393 }
394
395 anyhow::bail!(
396 "Secure storage not available. Cannot store bearer token for account '{}'.",
397 _account_id
398 )
399}
400
401#[allow(dead_code)]
403pub fn get_bearer_token_for_account(_account_id: &str) -> Result<Option<String>> {
404 #[cfg(feature = "secure-storage")]
405 {
406 if crate::config::secure_storage::is_available() {
407 let key = account_storage_key(_account_id, "bearer_token");
408 return crate::config::secure_storage::get_secret(&key);
409 }
410 }
411
412 Ok(None)
413}
414
415#[allow(dead_code)]
417pub fn delete_all_for_account(_account_id: &str) -> Result<()> {
418 delete_tokens_for_account(_account_id)?;
419
420 #[cfg(feature = "secure-storage")]
421 {
422 if crate::config::secure_storage::is_available() {
423 for key_type in ["api_key", "bearer_token"] {
424 let key = account_storage_key(_account_id, key_type);
425 let _ = crate::config::secure_storage::delete_secret(&key);
426 }
427 }
428 }
429
430 Ok(())
431}