busbar_sf_auth/
storage.rs1use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5
6use crate::error::{Error, ErrorKind, Result};
7use crate::oauth::TokenResponse;
8
9pub trait TokenStorage: Send + Sync {
11 fn save(&self, key: &str, token: &TokenResponse) -> Result<()>;
13
14 fn load(&self, key: &str) -> Result<Option<TokenResponse>>;
16
17 fn delete(&self, key: &str) -> Result<()>;
19
20 fn exists(&self, key: &str) -> Result<bool>;
22
23 fn list(&self) -> Result<Vec<String>>;
25}
26
27#[derive(Debug, Clone)]
29pub struct FileTokenStorage {
30 base_path: PathBuf,
31}
32
33impl FileTokenStorage {
34 pub fn new() -> Result<Self> {
38 let base_path = default_token_dir()?;
39 Ok(Self { base_path })
40 }
41
42 pub fn with_path(path: impl AsRef<Path>) -> Self {
44 Self {
45 base_path: path.as_ref().to_path_buf(),
46 }
47 }
48
49 fn token_path(&self, key: &str) -> PathBuf {
51 let safe_key = key
53 .chars()
54 .map(|c| {
55 if c.is_alphanumeric() || c == '-' || c == '_' {
56 c
57 } else {
58 '_'
59 }
60 })
61 .collect::<String>();
62
63 self.base_path.join(format!("{}.json", safe_key))
64 }
65
66 fn ensure_dir(&self) -> Result<()> {
68 if !self.base_path.exists() {
69 std::fs::create_dir_all(&self.base_path)?;
70 }
71 Ok(())
72 }
73}
74
75impl Default for FileTokenStorage {
76 fn default() -> Self {
77 Self::new().expect("Failed to create default token storage")
78 }
79}
80
81impl TokenStorage for FileTokenStorage {
82 fn save(&self, key: &str, token: &TokenResponse) -> Result<()> {
83 self.ensure_dir()?;
84
85 let path = self.token_path(key);
86 let stored = StoredToken {
87 token: token.clone(),
88 stored_at: chrono::Utc::now(),
89 };
90
91 let json = serde_json::to_string_pretty(&stored)?;
92 std::fs::write(&path, json)?;
93
94 #[cfg(unix)]
96 {
97 use std::os::unix::fs::PermissionsExt;
98 let perms = std::fs::Permissions::from_mode(0o600);
99 std::fs::set_permissions(&path, perms)?;
100 }
101
102 Ok(())
103 }
104
105 fn load(&self, key: &str) -> Result<Option<TokenResponse>> {
106 let path = self.token_path(key);
107
108 if !path.exists() {
109 return Ok(None);
110 }
111
112 let json = std::fs::read_to_string(&path)?;
113 let stored: StoredToken = serde_json::from_str(&json)?;
114
115 Ok(Some(stored.token))
116 }
117
118 fn delete(&self, key: &str) -> Result<()> {
119 let path = self.token_path(key);
120
121 if path.exists() {
122 std::fs::remove_file(&path)?;
123 }
124
125 Ok(())
126 }
127
128 fn exists(&self, key: &str) -> Result<bool> {
129 Ok(self.token_path(key).exists())
130 }
131
132 fn list(&self) -> Result<Vec<String>> {
133 if !self.base_path.exists() {
134 return Ok(Vec::new());
135 }
136
137 let mut keys = Vec::new();
138 for entry in std::fs::read_dir(&self.base_path)? {
139 let entry = entry?;
140 let path = entry.path();
141
142 if path.extension().map(|e| e == "json").unwrap_or(false) {
143 if let Some(stem) = path.file_stem() {
144 keys.push(stem.to_string_lossy().to_string());
145 }
146 }
147 }
148
149 Ok(keys)
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155struct StoredToken {
156 token: TokenResponse,
157 stored_at: chrono::DateTime<chrono::Utc>,
158}
159
160pub fn default_token_dir() -> Result<PathBuf> {
162 let home = dirs::home_dir().ok_or_else(|| {
163 Error::new(ErrorKind::Config(
164 "Could not find home directory".to_string(),
165 ))
166 })?;
167
168 Ok(home.join(".sf-api").join("tokens"))
169}
170
171#[allow(dead_code)]
173pub fn default_token_path(key: &str) -> Result<PathBuf> {
174 let dir = default_token_dir()?;
175 Ok(dir.join(format!("{}.json", key)))
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use tempfile::TempDir;
182
183 fn test_token() -> TokenResponse {
184 TokenResponse {
185 access_token: "test_access".to_string(),
186 refresh_token: Some("test_refresh".to_string()),
187 instance_url: "https://test.salesforce.com".to_string(),
188 id: None,
189 token_type: Some("Bearer".to_string()),
190 scope: None,
191 signature: None,
192 issued_at: None,
193 }
194 }
195
196 #[test]
197 fn test_file_storage_save_load() {
198 let temp_dir = TempDir::new().unwrap();
199 let storage = FileTokenStorage::with_path(temp_dir.path());
200
201 let token = test_token();
202 storage.save("test_org", &token).unwrap();
203
204 let loaded = storage.load("test_org").unwrap().unwrap();
205 assert_eq!(loaded.access_token, "test_access");
206 assert_eq!(loaded.refresh_token, Some("test_refresh".to_string()));
207 }
208
209 #[test]
210 fn test_file_storage_exists() {
211 let temp_dir = TempDir::new().unwrap();
212 let storage = FileTokenStorage::with_path(temp_dir.path());
213
214 assert!(!storage.exists("missing").unwrap());
215
216 storage.save("exists", &test_token()).unwrap();
217 assert!(storage.exists("exists").unwrap());
218 }
219
220 #[test]
221 fn test_file_storage_delete() {
222 let temp_dir = TempDir::new().unwrap();
223 let storage = FileTokenStorage::with_path(temp_dir.path());
224
225 storage.save("to_delete", &test_token()).unwrap();
226 assert!(storage.exists("to_delete").unwrap());
227
228 storage.delete("to_delete").unwrap();
229 assert!(!storage.exists("to_delete").unwrap());
230 }
231
232 #[test]
233 fn test_file_storage_list() {
234 let temp_dir = TempDir::new().unwrap();
235 let storage = FileTokenStorage::with_path(temp_dir.path());
236
237 storage.save("org1", &test_token()).unwrap();
238 storage.save("org2", &test_token()).unwrap();
239
240 let keys = storage.list().unwrap();
241 assert_eq!(keys.len(), 2);
242 assert!(keys.contains(&"org1".to_string()));
243 assert!(keys.contains(&"org2".to_string()));
244 }
245
246 #[test]
247 fn test_key_sanitization() {
248 let temp_dir = TempDir::new().unwrap();
249 let storage = FileTokenStorage::with_path(temp_dir.path());
250
251 storage.save("user@example.com", &test_token()).unwrap();
253
254 let path = storage.token_path("user@example.com");
255 assert!(path
256 .file_name()
257 .unwrap()
258 .to_str()
259 .unwrap()
260 .contains("user_example_com"));
261 }
262}