1use anyhow::{Context, Result};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12const SERVICE: &str = "smart-tree-proxy";
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct StoredToken {
16 pub access_token: String,
17 pub refresh_token: Option<String>,
18 pub expires_at: Option<DateTime<Utc>>,
19 pub scope: Option<String>,
20 pub token_type: Option<String>,
21}
22
23impl StoredToken {
24 pub fn is_expired(&self) -> bool {
25 match self.expires_at {
26 Some(exp) => Utc::now() >= exp - chrono::Duration::seconds(30),
27 None => false,
28 }
29 }
30}
31
32pub fn save(provider: &str, account: &str, token: &StoredToken) -> Result<()> {
34 let payload = serde_json::to_string(token)?;
35 let entry_user = format!("{}:{}", provider, account);
36
37 match keyring::Entry::new(SERVICE, &entry_user).and_then(|e| e.set_password(&payload)) {
38 Ok(()) => Ok(()),
39 Err(_) => save_file(provider, account, &payload),
40 }
41}
42
43pub fn load(provider: &str, account: &str) -> Result<Option<StoredToken>> {
44 let entry_user = format!("{}:{}", provider, account);
45
46 if let Ok(entry) = keyring::Entry::new(SERVICE, &entry_user) {
47 match entry.get_password() {
48 Ok(payload) => return Ok(Some(serde_json::from_str(&payload)?)),
49 Err(keyring::Error::NoEntry) => return Ok(None),
50 Err(_) => {}
51 }
52 }
53 load_file(provider, account)
54}
55
56pub fn delete(provider: &str, account: &str) -> Result<()> {
57 let entry_user = format!("{}:{}", provider, account);
58 if let Ok(entry) = keyring::Entry::new(SERVICE, &entry_user) {
59 let _ = entry.delete_credential();
60 }
61 let path = file_path(provider, account)?;
62 if path.exists() {
63 std::fs::remove_file(path)?;
64 }
65 Ok(())
66}
67
68fn fallback_dir() -> Result<PathBuf> {
69 let home = std::env::var("HOME").context("HOME not set")?;
70 let dir = PathBuf::from(home).join(".st").join("proxy_tokens");
71 std::fs::create_dir_all(&dir)?;
72 Ok(dir)
73}
74
75fn file_path(provider: &str, account: &str) -> Result<PathBuf> {
76 let safe = |s: &str| s.replace(['/', '\\', ':'], "_");
77 Ok(fallback_dir()?.join(format!("{}__{}.json", safe(provider), safe(account))))
78}
79
80fn save_file(provider: &str, account: &str, payload: &str) -> Result<()> {
81 let path = file_path(provider, account)?;
82 std::fs::write(&path, payload)?;
83 #[cfg(unix)]
84 {
85 use std::os::unix::fs::PermissionsExt;
86 let mut perms = std::fs::metadata(&path)?.permissions();
87 perms.set_mode(0o600);
88 std::fs::set_permissions(&path, perms)?;
89 }
90 Ok(())
91}
92
93fn load_file(provider: &str, account: &str) -> Result<Option<StoredToken>> {
94 let path = file_path(provider, account)?;
95 if !path.exists() {
96 return Ok(None);
97 }
98 let payload = std::fs::read_to_string(path)?;
99 Ok(Some(serde_json::from_str(&payload)?))
100}