nonce_auth/nonce/
storage.rs

1use async_trait::async_trait;
2use std::time::Duration;
3
4use crate::NonceError;
5
6/// Represents a stored nonce entry with its metadata.
7#[derive(Debug, Clone)]
8pub struct NonceEntry {
9    /// The unique nonce value
10    pub nonce: String,
11    /// Unix timestamp when the nonce was created
12    pub created_at: i64,
13    /// Optional context for nonce scoping
14    pub context: Option<String>,
15}
16
17/// Statistics about the nonce storage backend.
18#[derive(Debug, Clone)]
19pub struct StorageStats {
20    /// Total number of nonce records in storage
21    pub total_records: usize,
22    /// Additional backend-specific information
23    pub backend_info: String,
24}
25
26/// Abstract storage backend for nonce persistence.
27///
28/// This trait defines the interface that all storage backends must implement
29/// to work with the nonce authentication system. It provides operations for
30/// storing, retrieving, and managing nonces with expiration support.
31///
32/// # Thread Safety
33///
34/// All methods are async and must be thread-safe. Implementations should
35/// handle concurrent access properly.
36///
37/// # Error Handling
38///
39/// All methods return `Result<T, NonceError>` and should map backend-specific
40/// errors to appropriate `NonceError` variants.
41///
42/// # Example Implementation
43///
44/// ```rust
45/// use nonce_auth::storage::{NonceStorage, NonceEntry, StorageStats};
46/// use nonce_auth::NonceError;
47/// use async_trait::async_trait;
48/// use std::collections::HashMap;
49/// use std::sync::Arc;
50/// use std::time::Duration;
51/// use tokio::sync::RwLock;
52///
53/// #[derive(Default)]
54/// pub struct MemoryStorage {
55///     data: Arc<RwLock<HashMap<String, NonceEntry>>>,
56/// }
57///
58/// #[async_trait]
59/// impl NonceStorage for MemoryStorage {
60///     async fn get(&self, nonce: &str, context: Option<&str>) -> Result<Option<NonceEntry>, NonceError> {
61///         let key = format!("{}:{}", nonce, context.unwrap_or(""));
62///         let data = self.data.read().await;
63///         Ok(data.get(&key).cloned())
64///     }
65///
66///     async fn set(&self, nonce: &str, context: Option<&str>, ttl: Duration) -> Result<(), NonceError> {
67///         let key = format!("{}:{}", nonce, context.unwrap_or(""));
68///         let entry = NonceEntry {
69///             nonce: nonce.to_string(),
70///             created_at: std::time::SystemTime::now()
71///                 .duration_since(std::time::UNIX_EPOCH)
72///                 .unwrap()
73///                 .as_secs() as i64,
74///             context: context.map(|s| s.to_string()),
75///         };
76///         let mut data = self.data.write().await;
77///         if data.contains_key(&key) {
78///             return Err(NonceError::DuplicateNonce);
79///         }
80///         data.insert(key, entry);
81///         Ok(())
82///     }
83///
84///     async fn exists(&self, nonce: &str, context: Option<&str>) -> Result<bool, NonceError> {
85///         let key = format!("{}:{}", nonce, context.unwrap_or(""));
86///         let data = self.data.read().await;
87///         Ok(data.contains_key(&key))
88///     }
89///
90///     async fn cleanup_expired(&self, cutoff_time: i64) -> Result<usize, NonceError> {
91///         let mut data = self.data.write().await;
92///         let initial_count = data.len();
93///         data.retain(|_, entry| entry.created_at > cutoff_time);
94///         Ok(initial_count - data.len())
95///     }
96///
97///     async fn get_stats(&self) -> Result<StorageStats, NonceError> {
98///         let data = self.data.read().await;
99///         Ok(StorageStats {
100///             total_records: data.len(),
101///             backend_info: "In-memory HashMap storage".to_string(),
102///         })
103///     }
104/// }
105/// ```
106#[async_trait]
107pub trait NonceStorage: Send + Sync {
108    /// Retrieves a nonce entry if it exists.
109    ///
110    /// # Arguments
111    ///
112    /// * `nonce` - The nonce value to retrieve
113    /// * `context` - Optional context for scoping the nonce
114    ///
115    /// # Returns
116    ///
117    /// * `Ok(Some(NonceEntry))` - If the nonce exists and is not expired
118    /// * `Ok(None)` - If the nonce doesn't exist or has expired
119    /// * `Err(NonceError)` - If there was an error accessing storage
120    async fn get(
121        &self,
122        nonce: &str,
123        context: Option<&str>,
124    ) -> Result<Option<NonceEntry>, NonceError>;
125
126    /// Stores a new nonce with expiration time.
127    ///
128    /// This method should atomically check for duplicates and insert the nonce.
129    /// If the nonce already exists (considering context), it should return
130    /// `NonceError::DuplicateNonce`.
131    ///
132    /// # Arguments
133    ///
134    /// * `nonce` - The nonce value to store
135    /// * `context` - Optional context for scoping the nonce
136    /// * `ttl` - Time-to-live duration for the nonce
137    ///
138    /// # Returns
139    ///
140    /// * `Ok(())` - If the nonce was successfully stored
141    /// * `Err(NonceError::DuplicateNonce)` - If the nonce already exists
142    /// * `Err(NonceError)` - If there was an error accessing storage
143    async fn set(
144        &self,
145        nonce: &str,
146        context: Option<&str>,
147        ttl: Duration,
148    ) -> Result<(), NonceError>;
149
150    /// Checks if a nonce exists without retrieving it.
151    ///
152    /// This is an optimization method for cases where only existence
153    /// checking is needed without the full entry data.
154    ///
155    /// # Arguments
156    ///
157    /// * `nonce` - The nonce value to check
158    /// * `context` - Optional context for scoping the nonce
159    ///
160    /// # Returns
161    ///
162    /// * `Ok(true)` - If the nonce exists and is not expired
163    /// * `Ok(false)` - If the nonce doesn't exist or has expired
164    /// * `Err(NonceError)` - If there was an error accessing storage
165    async fn exists(&self, nonce: &str, context: Option<&str>) -> Result<bool, NonceError>;
166
167    /// Removes all expired nonces from storage.
168    ///
169    /// This method should remove all nonces that were created before
170    /// the specified cutoff time.
171    ///
172    /// # Arguments
173    ///
174    /// * `cutoff_time` - Unix timestamp; nonces created before this time should be removed
175    ///
176    /// # Returns
177    ///
178    /// * `Ok(count)` - Number of nonces that were removed
179    /// * `Err(NonceError)` - If there was an error accessing storage
180    async fn cleanup_expired(&self, cutoff_time: i64) -> Result<usize, NonceError>;
181
182    /// Returns statistics about the storage backend.
183    ///
184    /// This method provides insight into the current state of the storage
185    /// backend, which can be useful for monitoring and debugging.
186    ///
187    /// # Returns
188    ///
189    /// * `Ok(StorageStats)` - Current storage statistics
190    /// * `Err(NonceError)` - If there was an error accessing storage
191    async fn get_stats(&self) -> Result<StorageStats, NonceError>;
192
193    /// Optional method for storage backend initialization.
194    ///
195    /// This method is called once when the storage backend is first used.
196    /// Implementations can use this for tasks like schema creation,
197    /// connection setup, etc.
198    ///
199    /// # Returns
200    ///
201    /// * `Ok(())` - If initialization succeeded
202    /// * `Err(NonceError)` - If initialization failed
203    async fn init(&self) -> Result<(), NonceError> {
204        // Default implementation does nothing
205        Ok(())
206    }
207}
208
209/// A simple in-memory storage implementation for testing and demonstration.
210///
211/// This implementation uses a `HashMap` wrapped in `Arc<RwLock<>>` for
212/// thread-safe access. It doesn't persist data across restarts and doesn't
213/// implement automatic expiration (expired entries are only removed during
214/// cleanup operations).
215///
216/// # Thread Safety
217///
218/// This implementation is fully thread-safe and can handle concurrent
219/// operations from multiple threads.
220///
221/// # Usage
222///
223/// ```rust
224/// use nonce_auth::storage::{MemoryStorage, NonceStorage};
225/// use std::time::Duration;
226///
227/// # async fn example() -> Result<(), nonce_auth::NonceError> {
228/// let storage = MemoryStorage::new();
229///
230/// // Store a nonce
231/// storage.set("test-nonce", None, Duration::from_secs(300)).await?;
232///
233/// // Check if it exists
234/// let exists = storage.exists("test-nonce", None).await?;
235/// assert!(exists);
236/// # Ok(())
237/// # }
238/// ```
239#[derive(Debug, Default)]
240pub struct MemoryStorage {
241    data: std::sync::Arc<tokio::sync::RwLock<std::collections::HashMap<String, NonceEntry>>>,
242}
243
244impl MemoryStorage {
245    /// Creates a new in-memory storage instance.
246    pub fn new() -> Self {
247        Self::default()
248    }
249
250    /// Creates a storage key from nonce and context.
251    fn make_key(nonce: &str, context: Option<&str>) -> String {
252        format!("{nonce}:{}", context.unwrap_or(""))
253    }
254}
255
256#[async_trait]
257impl NonceStorage for MemoryStorage {
258    async fn get(
259        &self,
260        nonce: &str,
261        context: Option<&str>,
262    ) -> Result<Option<NonceEntry>, NonceError> {
263        let key = Self::make_key(nonce, context);
264        let data = self.data.read().await;
265        Ok(data.get(&key).cloned())
266    }
267
268    async fn set(
269        &self,
270        nonce: &str,
271        context: Option<&str>,
272        _ttl: Duration,
273    ) -> Result<(), NonceError> {
274        let key = Self::make_key(nonce, context);
275        let entry = NonceEntry {
276            nonce: nonce.to_string(),
277            created_at: std::time::SystemTime::now()
278                .duration_since(std::time::UNIX_EPOCH)
279                .map_err(|e| NonceError::CryptoError(format!("System clock error: {e}")))?
280                .as_secs() as i64,
281            context: context.map(|s| s.to_string()),
282        };
283
284        let mut data = self.data.write().await;
285        if data.contains_key(&key) {
286            return Err(NonceError::DuplicateNonce);
287        }
288        data.insert(key, entry);
289        Ok(())
290    }
291
292    async fn exists(&self, nonce: &str, context: Option<&str>) -> Result<bool, NonceError> {
293        let key = Self::make_key(nonce, context);
294        let data = self.data.read().await;
295        Ok(data.contains_key(&key))
296    }
297
298    async fn cleanup_expired(&self, cutoff_time: i64) -> Result<usize, NonceError> {
299        let mut data = self.data.write().await;
300        let initial_count = data.len();
301        data.retain(|_, entry| entry.created_at > cutoff_time);
302        Ok(initial_count - data.len())
303    }
304
305    async fn get_stats(&self) -> Result<StorageStats, NonceError> {
306        let data = self.data.read().await;
307        Ok(StorageStats {
308            total_records: data.len(),
309            backend_info: "In-memory HashMap storage".to_string(),
310        })
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use std::time::{SystemTime, UNIX_EPOCH};
318
319    #[tokio::test]
320    async fn test_memory_storage_basic_operations() -> Result<(), NonceError> {
321        let storage = MemoryStorage::new();
322
323        // Test set and exists
324        storage
325            .set("test-nonce", None, Duration::from_secs(300))
326            .await?;
327        assert!(storage.exists("test-nonce", None).await?);
328
329        // Test get
330        let entry = storage.get("test-nonce", None).await?;
331        assert!(entry.is_some());
332        let entry = entry.unwrap();
333        assert_eq!(entry.nonce, "test-nonce");
334        assert!(entry.context.is_none());
335
336        Ok(())
337    }
338
339    #[tokio::test]
340    async fn test_memory_storage_duplicate_nonce() -> Result<(), NonceError> {
341        let storage = MemoryStorage::new();
342
343        // First set should succeed
344        storage
345            .set("test-nonce", None, Duration::from_secs(300))
346            .await?;
347
348        // Second set should fail
349        let result = storage
350            .set("test-nonce", None, Duration::from_secs(300))
351            .await;
352        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
353
354        Ok(())
355    }
356
357    #[tokio::test]
358    async fn test_memory_storage_context_isolation() -> Result<(), NonceError> {
359        let storage = MemoryStorage::new();
360
361        // Same nonce, different contexts should work
362        storage
363            .set("test-nonce", Some("context1"), Duration::from_secs(300))
364            .await?;
365        storage
366            .set("test-nonce", Some("context2"), Duration::from_secs(300))
367            .await?;
368
369        // Both should exist
370        assert!(storage.exists("test-nonce", Some("context1")).await?);
371        assert!(storage.exists("test-nonce", Some("context2")).await?);
372
373        // But not in wrong context
374        assert!(!storage.exists("test-nonce", Some("context3")).await?);
375
376        Ok(())
377    }
378
379    #[tokio::test]
380    async fn test_memory_storage_cleanup() -> Result<(), NonceError> {
381        let storage = MemoryStorage::new();
382
383        // Add some nonces
384        storage
385            .set("old-nonce", None, Duration::from_secs(300))
386            .await?;
387        storage
388            .set("new-nonce", None, Duration::from_secs(300))
389            .await?;
390
391        // Cleanup with cutoff time in the future should remove all
392        let future_time = SystemTime::now()
393            .duration_since(UNIX_EPOCH)
394            .unwrap()
395            .as_secs() as i64
396            + 3600;
397
398        let removed = storage.cleanup_expired(future_time).await?;
399        assert_eq!(removed, 2);
400
401        // Both should be gone
402        assert!(!storage.exists("old-nonce", None).await?);
403        assert!(!storage.exists("new-nonce", None).await?);
404
405        Ok(())
406    }
407
408    #[tokio::test]
409    async fn test_memory_storage_stats() -> Result<(), NonceError> {
410        let storage = MemoryStorage::new();
411
412        // Initial stats
413        let stats = storage.get_stats().await?;
414        assert_eq!(stats.total_records, 0);
415
416        // Add some nonces
417        storage
418            .set("nonce1", None, Duration::from_secs(300))
419            .await?;
420        storage
421            .set("nonce2", Some("context"), Duration::from_secs(300))
422            .await?;
423
424        // Updated stats
425        let stats = storage.get_stats().await?;
426        assert_eq!(stats.total_records, 2);
427        assert!(stats.backend_info.contains("In-memory"));
428
429        Ok(())
430    }
431}