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!(
90 "Saving machine token for gateway: {}",
91 token.gateway_code
92 );
93
94 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
96 tracing::error!("Failed to serialize machine token: {}", e);
97 StorageError::Config(format!("JSON serialization failed: {}", e))
98 })?;
99
100 storage.write_file_str(TOKEN_STORAGE_PATH, &json).await?;
102
103 tracing::info!(
104 "Machine token saved successfully: gateway_id={}, expires_at={}",
105 token.gateway_id,
106 token.expires_at
107 );
108
109 Ok(())
110}
111
112pub async fn load_token(
126 storage: &dyn StorageBackend,
127) -> Result<Option<MachineToken>, StorageError> {
128 tracing::debug!("Loading machine token from storage");
129
130 if !storage.exists_str(TOKEN_STORAGE_PATH) {
132 tracing::debug!("No machine token file found");
133 return Ok(None);
134 }
135
136 let json = storage.read_file_str(TOKEN_STORAGE_PATH).await?;
138
139 let token: MachineToken = serde_json::from_slice(&json).map_err(|e| {
141 tracing::error!("Failed to deserialize machine token: {}", e);
142 StorageError::Config(format!("JSON deserialization failed: {}", e))
143 })?;
144
145 tracing::debug!(
146 "Machine token loaded: gateway_id={}, expires_at={}, valid={}",
147 token.gateway_id,
148 token.expires_at,
149 token.is_valid()
150 );
151
152 Ok(Some(token))
153}
154
155pub async fn clear_token(storage: &dyn StorageBackend) -> Result<(), StorageError> {
168 tracing::debug!("Clearing machine token from storage");
169
170 if !storage.exists_str(TOKEN_STORAGE_PATH) {
172 tracing::debug!("No machine token file to clear");
173 return Ok(());
174 }
175
176 storage.remove_str(TOKEN_STORAGE_PATH).await?;
178
179 tracing::info!("Machine token cleared successfully");
180
181 Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::storage::FilesystemStorage;
188 use tempfile::TempDir;
189
190 #[test]
191 fn test_machine_token_creation() {
192 let token = MachineToken::new(
193 "test_token".to_string(),
194 "2025-12-31T23:59:59Z".to_string(),
195 "gw123".to_string(),
196 "gateway-code-123".to_string(),
197 vec!["harmony:send".to_string(), "harmony:receive".to_string()],
198 );
199
200 assert_eq!(token.machine_token, "test_token");
201 assert_eq!(token.gateway_id, "gw123");
202 assert_eq!(token.gateway_code, "gateway-code-123");
203 assert_eq!(token.abilities.len(), 2);
204 assert!(!token.issued_at.is_empty());
205 }
206
207 #[test]
208 fn test_machine_token_is_expired() {
209 let expired_token = MachineToken::new(
211 "test_token".to_string(),
212 "2020-01-01T00:00:00Z".to_string(),
213 "gw123".to_string(),
214 "gateway-code-123".to_string(),
215 vec![],
216 );
217 assert!(expired_token.is_expired());
218 assert!(!expired_token.is_valid());
219
220 let valid_token = MachineToken::new(
222 "test_token".to_string(),
223 "2099-12-31T23:59:59Z".to_string(),
224 "gw123".to_string(),
225 "gateway-code-123".to_string(),
226 vec![],
227 );
228 assert!(!valid_token.is_expired());
229 assert!(valid_token.is_valid());
230 }
231
232 #[test]
233 fn test_machine_token_serialization() {
234 let token = MachineToken::new(
235 "test_token".to_string(),
236 "2025-12-31T23:59:59Z".to_string(),
237 "gw123".to_string(),
238 "gateway-code-123".to_string(),
239 vec!["harmony:send".to_string()],
240 );
241
242 let json = serde_json::to_string(&token).unwrap();
243 assert!(json.contains("\"machine_token\":\"test_token\""));
244 assert!(json.contains("\"gateway_id\":\"gw123\""));
245 assert!(json.contains("\"gateway_code\":\"gateway-code-123\""));
246
247 let deserialized: MachineToken = serde_json::from_str(&json).unwrap();
249 assert_eq!(deserialized.machine_token, token.machine_token);
250 assert_eq!(deserialized.gateway_id, token.gateway_id);
251 }
252
253 #[tokio::test]
254 async fn test_save_and_load_token() {
255 let temp_dir = TempDir::new().unwrap();
256 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
257
258 let token = MachineToken::new(
259 "test_token".to_string(),
260 "2025-12-31T23:59:59Z".to_string(),
261 "gw123".to_string(),
262 "gateway-code-123".to_string(),
263 vec!["harmony:send".to_string()],
264 );
265
266 save_token(&storage, &token).await.unwrap();
268
269 let loaded = load_token(&storage).await.unwrap();
271 assert!(loaded.is_some());
272
273 let loaded_token = loaded.unwrap();
274 assert_eq!(loaded_token.machine_token, token.machine_token);
275 assert_eq!(loaded_token.gateway_id, token.gateway_id);
276 assert_eq!(loaded_token.gateway_code, token.gateway_code);
277 }
278
279 #[tokio::test]
280 async fn test_load_nonexistent_token() {
281 let temp_dir = TempDir::new().unwrap();
282 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
283
284 let result = load_token(&storage).await.unwrap();
286 assert!(result.is_none());
287 }
288
289 #[tokio::test]
290 async fn test_clear_token() {
291 let temp_dir = TempDir::new().unwrap();
292 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
293
294 let token = MachineToken::new(
295 "test_token".to_string(),
296 "2025-12-31T23:59:59Z".to_string(),
297 "gw123".to_string(),
298 "gateway-code-123".to_string(),
299 vec![],
300 );
301
302 save_token(&storage, &token).await.unwrap();
304
305 assert!(load_token(&storage).await.unwrap().is_some());
307
308 clear_token(&storage).await.unwrap();
310
311 assert!(load_token(&storage).await.unwrap().is_none());
313 }
314
315 #[tokio::test]
316 async fn test_clear_nonexistent_token() {
317 let temp_dir = TempDir::new().unwrap();
318 let storage = FilesystemStorage::new(temp_dir.path()).unwrap();
319
320 clear_token(&storage).await.unwrap();
322 }
323}