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}