1use crate::storage::{StorageBackend, StorageError};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5const TOKEN_STORAGE_PATH: &str = "runbeam/auth.json";
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct MachineToken {
14 pub machine_token: String,
16 pub expires_at: String,
18 pub gateway_id: String,
20 pub gateway_code: String,
22 #[serde(default)]
24 pub abilities: Vec<String>,
25 pub issued_at: String,
27}
28
29impl MachineToken {
30 pub fn new(
32 machine_token: String,
33 expires_at: String,
34 gateway_id: String,
35 gateway_code: String,
36 abilities: Vec<String>,
37 ) -> Self {
38 let issued_at = Utc::now().to_rfc3339();
39
40 Self {
41 machine_token,
42 expires_at,
43 gateway_id,
44 gateway_code,
45 abilities,
46 issued_at,
47 }
48 }
49
50 pub fn is_expired(&self) -> bool {
52 match DateTime::parse_from_rfc3339(&self.expires_at) {
54 Ok(expiry) => {
55 let now = Utc::now();
56 expiry.with_timezone(&Utc) < now
57 }
58 Err(e) => {
59 tracing::warn!("Failed to parse token expiry date: {}", e);
60 true
62 }
63 }
64 }
65
66 pub fn is_valid(&self) -> bool {
68 !self.is_expired()
69 }
70}
71
72pub async fn save_token(
86 storage: &dyn StorageBackend,
87 token: &MachineToken,
88) -> Result<(), StorageError> {
89 tracing::debug!("Saving machine token for gateway: {}", token.gateway_code);
90
91 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
93 tracing::error!("Failed to serialize machine token: {}", e);
94 StorageError::Config(format!("JSON serialization failed: {}", e))
95 })?;
96
97 storage.write_file_str(TOKEN_STORAGE_PATH, &json).await?;
99
100 tracing::info!(
101 "Machine token saved successfully: gateway_id={}, expires_at={}",
102 token.gateway_id,
103 token.expires_at
104 );
105
106 Ok(())
107}
108
109pub async fn load_token(
123 storage: &dyn StorageBackend,
124) -> Result<Option<MachineToken>, StorageError> {
125 tracing::debug!("Loading machine token from storage");
126
127 if !storage.exists_str(TOKEN_STORAGE_PATH) {
129 tracing::debug!("No machine token file found");
130 return Ok(None);
131 }
132
133 let json = storage.read_file_str(TOKEN_STORAGE_PATH).await?;
135
136 let token: MachineToken = serde_json::from_slice(&json).map_err(|e| {
138 tracing::error!("Failed to deserialize machine token: {}", e);
139 StorageError::Config(format!("JSON deserialization failed: {}", e))
140 })?;
141
142 tracing::debug!(
143 "Machine token loaded: gateway_id={}, expires_at={}, valid={}",
144 token.gateway_id,
145 token.expires_at,
146 token.is_valid()
147 );
148
149 Ok(Some(token))
150}
151
152pub async fn clear_token(storage: &dyn StorageBackend) -> Result<(), StorageError> {
165 tracing::debug!("Clearing machine token from storage");
166
167 if !storage.exists_str(TOKEN_STORAGE_PATH) {
169 tracing::debug!("No machine token file to clear");
170 return Ok(());
171 }
172
173 storage.remove_str(TOKEN_STORAGE_PATH).await?;
175
176 tracing::info!("Machine token cleared successfully");
177
178 Ok(())
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::storage::FilesystemStorage;
185 use tempfile::TempDir;
186
187 #[test]
188 fn test_machine_token_creation() {
189 let token = MachineToken::new(
190 "test_token".to_string(),
191 "2025-12-31T23:59:59Z".to_string(),
192 "gw123".to_string(),
193 "gateway-code-123".to_string(),
194 vec!["harmony:send".to_string(), "harmony:receive".to_string()],
195 );
196
197 assert_eq!(token.machine_token, "test_token");
198 assert_eq!(token.gateway_id, "gw123");
199 assert_eq!(token.gateway_code, "gateway-code-123");
200 assert_eq!(token.abilities.len(), 2);
201 assert!(!token.issued_at.is_empty());
202 }
203
204 #[test]
205 fn test_machine_token_is_expired() {
206 let expired_token = MachineToken::new(
208 "test_token".to_string(),
209 "2020-01-01T00:00:00Z".to_string(),
210 "gw123".to_string(),
211 "gateway-code-123".to_string(),
212 vec![],
213 );
214 assert!(expired_token.is_expired());
215 assert!(!expired_token.is_valid());
216
217 let valid_token = MachineToken::new(
219 "test_token".to_string(),
220 "2099-12-31T23:59:59Z".to_string(),
221 "gw123".to_string(),
222 "gateway-code-123".to_string(),
223 vec![],
224 );
225 assert!(!valid_token.is_expired());
226 assert!(valid_token.is_valid());
227 }
228
229 #[test]
230 fn test_machine_token_serialization() {
231 let token = MachineToken::new(
232 "test_token".to_string(),
233 "2025-12-31T23:59:59Z".to_string(),
234 "gw123".to_string(),
235 "gateway-code-123".to_string(),
236 vec!["harmony:send".to_string()],
237 );
238
239 let json = serde_json::to_string(&token).unwrap();
240 assert!(json.contains("\"machine_token\":\"test_token\""));
241 assert!(json.contains("\"gateway_id\":\"gw123\""));
242 assert!(json.contains("\"gateway_code\":\"gateway-code-123\""));
243
244 let deserialized: MachineToken = serde_json::from_str(&json).unwrap();
246 assert_eq!(deserialized.machine_token, token.machine_token);
247 assert_eq!(deserialized.gateway_id, token.gateway_id);
248 }
249
250 #[tokio::test]
251 async fn test_save_and_load_token() {
252 let temp_dir = TempDir::new().unwrap();
253 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
254
255 let token = MachineToken::new(
256 "test_token".to_string(),
257 "2025-12-31T23:59:59Z".to_string(),
258 "gw123".to_string(),
259 "gateway-code-123".to_string(),
260 vec!["harmony:send".to_string()],
261 );
262
263 save_token(&storage, &token).await.unwrap();
265
266 let loaded = load_token(&storage).await.unwrap();
268 assert!(loaded.is_some());
269
270 let loaded_token = loaded.unwrap();
271 assert_eq!(loaded_token.machine_token, token.machine_token);
272 assert_eq!(loaded_token.gateway_id, token.gateway_id);
273 assert_eq!(loaded_token.gateway_code, token.gateway_code);
274 }
275
276 #[tokio::test]
277 async fn test_load_nonexistent_token() {
278 let temp_dir = TempDir::new().unwrap();
279 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
280
281 let result = load_token(&storage).await.unwrap();
283 assert!(result.is_none());
284 }
285
286 #[tokio::test]
287 async fn test_clear_token() {
288 let temp_dir = TempDir::new().unwrap();
289 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
290
291 let token = MachineToken::new(
292 "test_token".to_string(),
293 "2025-12-31T23:59:59Z".to_string(),
294 "gw123".to_string(),
295 "gateway-code-123".to_string(),
296 vec![],
297 );
298
299 save_token(&storage, &token).await.unwrap();
301
302 assert!(load_token(&storage).await.unwrap().is_some());
304
305 clear_token(&storage).await.unwrap();
307
308 assert!(load_token(&storage).await.unwrap().is_none());
310 }
311
312 #[tokio::test]
313 async fn test_clear_nonexistent_token() {
314 let temp_dir = TempDir::new().unwrap();
315 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
316
317 clear_token(&storage).await.unwrap();
319 }
320}