Skip to main content

reinhardt_auth/sessions/backends/
cache.rs

1//! Cache-based session backend
2//!
3//! This module provides session storage using cache backends like Redis or in-memory cache.
4//!
5//! ## Example
6//!
7//! ```rust
8//! use reinhardt_auth::sessions::backends::{InMemorySessionBackend, SessionBackend};
9//! use serde_json::json;
10//!
11//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! // Create an in-memory session backend
13//! let backend = InMemorySessionBackend::new();
14//!
15//! // Store user session with login data
16//! let session_data = json!({
17//!     "user_id": 123,
18//!     "username": "bob",
19//!     "last_login": "2024-01-15T10:30:00Z",
20//! });
21//!
22//! backend.save("session_xyz", &session_data, Some(3600)).await?;
23//!
24//! // Load session data
25//! let loaded: Option<serde_json::Value> = backend.load("session_xyz").await?;
26//! assert!(loaded.is_some());
27//! assert_eq!(loaded.unwrap()["user_id"], 123);
28//! # Ok(())
29//! # }
30//! ```
31
32use async_trait::async_trait;
33use chrono::{DateTime, Utc};
34use reinhardt_utils::cache::{Cache, InMemoryCache};
35use serde::{Deserialize, Serialize};
36use std::sync::Arc;
37use thiserror::Error;
38
39use crate::sessions::cleanup::{CleanupableBackend, SessionMetadata};
40
41/// Session backend errors
42#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Eq, Error)]
44pub enum SessionError {
45	/// An error occurred in the cache backend.
46	#[error("Cache error: {0}")]
47	CacheError(String),
48	/// Session data could not be serialized or deserialized.
49	#[error("Serialization error: {0}")]
50	SerializationError(String),
51	/// The session has expired due to inactivity.
52	#[error("Session has expired due to inactivity")]
53	SessionExpired,
54}
55
56/// Session backend trait
57#[async_trait]
58pub trait SessionBackend: Send + Sync + Clone {
59	/// Load session data by key
60	async fn load<T>(&self, session_key: &str) -> Result<Option<T>, SessionError>
61	where
62		T: for<'de> Deserialize<'de> + Serialize + Send + Sync;
63
64	/// Save session data with optional TTL (in seconds)
65	async fn save<T>(
66		&self,
67		session_key: &str,
68		data: &T,
69		ttl: Option<u64>,
70	) -> Result<(), SessionError>
71	where
72		T: Serialize + Send + Sync;
73
74	/// Delete session by key
75	async fn delete(&self, session_key: &str) -> Result<(), SessionError>;
76
77	/// Check if session exists
78	async fn exists(&self, session_key: &str) -> Result<bool, SessionError>;
79}
80
81/// In-memory session backend
82///
83/// Stores sessions in memory using the InMemoryCache backend.
84/// Sessions are lost when the application restarts.
85///
86/// ## Example
87///
88/// ```rust
89/// use reinhardt_auth::sessions::backends::{InMemorySessionBackend, SessionBackend};
90/// use serde_json::json;
91///
92/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
93/// let backend = InMemorySessionBackend::new();
94///
95/// // Store shopping cart in session
96/// let cart_data = json!({
97///     "items": ["item1", "item2"],
98///     "total": 59.99,
99/// });
100///
101/// backend.save("cart_session_456", &cart_data, Some(1800)).await?;
102///
103/// // Check if session exists
104/// assert!(backend.exists("cart_session_456").await?);
105/// # Ok(())
106/// # }
107/// ```
108#[derive(Clone)]
109pub struct InMemorySessionBackend {
110	cache: Arc<InMemoryCache>,
111}
112
113impl InMemorySessionBackend {
114	/// Create a new in-memory session backend
115	pub fn new() -> Self {
116		Self {
117			cache: Arc::new(InMemoryCache::new()),
118		}
119	}
120}
121
122impl Default for InMemorySessionBackend {
123	fn default() -> Self {
124		Self::new()
125	}
126}
127
128#[async_trait]
129impl SessionBackend for InMemorySessionBackend {
130	async fn load<T>(&self, session_key: &str) -> Result<Option<T>, SessionError>
131	where
132		T: for<'de> Deserialize<'de> + Serialize + Send + Sync,
133	{
134		self.cache
135			.get(session_key)
136			.await
137			.map_err(|e| SessionError::CacheError(e.to_string()))
138	}
139
140	async fn save<T>(
141		&self,
142		session_key: &str,
143		data: &T,
144		ttl: Option<u64>,
145	) -> Result<(), SessionError>
146	where
147		T: Serialize + Send + Sync,
148	{
149		let duration = ttl.map(std::time::Duration::from_secs);
150		self.cache
151			.set(session_key, data, duration)
152			.await
153			.map_err(|e| SessionError::CacheError(e.to_string()))
154	}
155
156	async fn delete(&self, session_key: &str) -> Result<(), SessionError> {
157		self.cache
158			.delete(session_key)
159			.await
160			.map_err(|e| SessionError::CacheError(e.to_string()))
161	}
162
163	async fn exists(&self, session_key: &str) -> Result<bool, SessionError> {
164		self.cache
165			.has_key(session_key)
166			.await
167			.map_err(|e| SessionError::CacheError(e.to_string()))
168	}
169}
170
171#[async_trait]
172impl CleanupableBackend for InMemorySessionBackend {
173	/// Get all session keys
174	///
175	/// Returns all session keys stored in the backend,
176	/// including expired sessions that haven't been cleaned up.
177	async fn get_all_keys(&self) -> Result<Vec<String>, SessionError> {
178		Ok(self.cache.list_keys().await)
179	}
180
181	/// Get session metadata
182	///
183	/// Returns metadata for the specified session.
184	/// Returns `None` if the session does not exist.
185	async fn get_metadata(
186		&self,
187		session_key: &str,
188	) -> Result<Option<SessionMetadata>, SessionError> {
189		match self.cache.inspect_entry_with_timestamps(session_key).await {
190			Ok(Some((created, accessed))) => Ok(Some(SessionMetadata {
191				created_at: DateTime::<Utc>::from(created),
192				last_accessed: accessed.map(DateTime::<Utc>::from),
193			})),
194			Ok(None) => Ok(None),
195			Err(e) => Err(SessionError::CacheError(e.to_string())),
196		}
197	}
198}
199
200/// Cache-based session backend
201///
202/// Generic session backend that works with any cache implementation.
203///
204/// ## Example
205///
206/// ```rust
207/// use reinhardt_auth::sessions::backends::{CacheSessionBackend, SessionBackend};
208/// use reinhardt_utils::cache::InMemoryCache;
209/// use serde_json::json;
210/// use std::sync::Arc;
211///
212/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
213/// let cache = Arc::new(InMemoryCache::new());
214/// let backend = CacheSessionBackend::new(cache);
215///
216/// // Store user preferences in session
217/// let preferences = json!({
218///     "theme": "dark",
219///     "language": "en",
220///     "notifications": true,
221/// });
222///
223/// backend.save("pref_session_789", &preferences, Some(7200)).await?;
224///
225/// // Load preferences
226/// let loaded: Option<serde_json::Value> = backend.load("pref_session_789").await?;
227/// assert_eq!(loaded.unwrap()["theme"], "dark");
228/// # Ok(())
229/// # }
230/// ```
231#[derive(Clone)]
232pub struct CacheSessionBackend<C: Cache + Clone> {
233	cache: Arc<C>,
234}
235
236impl<C: Cache + Clone> CacheSessionBackend<C> {
237	/// Create a new cache-based session backend
238	pub fn new(cache: Arc<C>) -> Self {
239		Self { cache }
240	}
241}
242
243#[async_trait]
244impl<C: Cache + Clone + 'static> SessionBackend for CacheSessionBackend<C> {
245	async fn load<T>(&self, session_key: &str) -> Result<Option<T>, SessionError>
246	where
247		T: for<'de> Deserialize<'de> + Serialize + Send + Sync,
248	{
249		self.cache
250			.get(session_key)
251			.await
252			.map_err(|e| SessionError::CacheError(e.to_string()))
253	}
254
255	async fn save<T>(
256		&self,
257		session_key: &str,
258		data: &T,
259		ttl: Option<u64>,
260	) -> Result<(), SessionError>
261	where
262		T: Serialize + Send + Sync,
263	{
264		let duration = ttl.map(std::time::Duration::from_secs);
265		self.cache
266			.set(session_key, data, duration)
267			.await
268			.map_err(|e| SessionError::CacheError(e.to_string()))
269	}
270
271	async fn delete(&self, session_key: &str) -> Result<(), SessionError> {
272		self.cache
273			.delete(session_key)
274			.await
275			.map_err(|e| SessionError::CacheError(e.to_string()))
276	}
277
278	async fn exists(&self, session_key: &str) -> Result<bool, SessionError> {
279		self.cache
280			.has_key(session_key)
281			.await
282			.map_err(|e| SessionError::CacheError(e.to_string()))
283	}
284}
285
286#[cfg(test)]
287mod tests {
288	use super::*;
289	use rstest::rstest;
290	use serde_json::json;
291	use std::collections::HashMap;
292
293	#[rstest]
294	#[tokio::test]
295	async fn test_in_memory_save_and_load_roundtrip() {
296		// Arrange
297		let backend = InMemorySessionBackend::new();
298		let mut data = HashMap::new();
299		data.insert("user_id".to_string(), json!(42));
300		data.insert("username".to_string(), json!("alice"));
301
302		// Act
303		backend.save("sess_1", &data, Some(3600)).await.unwrap();
304		let loaded: Option<HashMap<String, serde_json::Value>> =
305			backend.load("sess_1").await.unwrap();
306
307		// Assert
308		let loaded = loaded.unwrap();
309		assert_eq!(loaded["user_id"], json!(42));
310		assert_eq!(loaded["username"], json!("alice"));
311	}
312
313	#[rstest]
314	#[tokio::test]
315	async fn test_in_memory_load_nonexistent_key() {
316		// Arrange
317		let backend = InMemorySessionBackend::new();
318
319		// Act
320		let loaded: Option<serde_json::Value> = backend.load("nonexistent").await.unwrap();
321
322		// Assert
323		assert!(loaded.is_none());
324	}
325
326	#[rstest]
327	#[tokio::test]
328	async fn test_in_memory_delete_removes_session() {
329		// Arrange
330		let backend = InMemorySessionBackend::new();
331		let data = json!({"key": "value"});
332		backend.save("sess_del", &data, Some(3600)).await.unwrap();
333
334		// Act
335		backend.delete("sess_del").await.unwrap();
336		let loaded: Option<serde_json::Value> = backend.load("sess_del").await.unwrap();
337
338		// Assert
339		assert!(loaded.is_none());
340	}
341
342	#[rstest]
343	#[tokio::test]
344	async fn test_in_memory_exists_reflects_state() {
345		// Arrange
346		let backend = InMemorySessionBackend::new();
347		let data = json!({"active": true});
348
349		// Assert - initially does not exist
350		assert!(!backend.exists("sess_ex").await.unwrap());
351
352		// Act - save
353		backend.save("sess_ex", &data, Some(3600)).await.unwrap();
354
355		// Assert - exists after save
356		assert!(backend.exists("sess_ex").await.unwrap());
357
358		// Act - delete
359		backend.delete("sess_ex").await.unwrap();
360
361		// Assert - does not exist after delete
362		assert!(!backend.exists("sess_ex").await.unwrap());
363	}
364
365	#[rstest]
366	#[tokio::test]
367	async fn test_in_memory_save_overwrites_existing() {
368		// Arrange
369		let backend = InMemorySessionBackend::new();
370		let data_v1 = json!({"version": 1});
371		let data_v2 = json!({"version": 2});
372
373		// Act
374		backend.save("sess_ow", &data_v1, Some(3600)).await.unwrap();
375		backend.save("sess_ow", &data_v2, Some(3600)).await.unwrap();
376		let loaded: Option<serde_json::Value> = backend.load("sess_ow").await.unwrap();
377
378		// Assert
379		assert_eq!(loaded.unwrap()["version"], 2);
380	}
381
382	#[rstest]
383	#[tokio::test]
384	async fn test_in_memory_save_with_ttl() {
385		// Arrange
386		let backend = InMemorySessionBackend::new();
387		let data = json!({"ttl_test": true});
388
389		// Act
390		backend.save("sess_ttl", &data, Some(60)).await.unwrap();
391		let loaded: Option<serde_json::Value> = backend.load("sess_ttl").await.unwrap();
392
393		// Assert
394		assert_eq!(loaded.unwrap()["ttl_test"], true);
395	}
396
397	#[rstest]
398	#[tokio::test]
399	async fn test_cache_backend_wrapper_save_and_load() {
400		// Arrange
401		let cache = Arc::new(InMemoryCache::new());
402		let backend = CacheSessionBackend::new(cache);
403		let data = json!({"wrapped": "value", "count": 99});
404
405		// Act
406		backend
407			.save("wrapped_sess", &data, Some(3600))
408			.await
409			.unwrap();
410		let loaded: Option<serde_json::Value> = backend.load("wrapped_sess").await.unwrap();
411
412		// Assert
413		let loaded = loaded.unwrap();
414		assert_eq!(loaded["wrapped"], "value");
415		assert_eq!(loaded["count"], 99);
416	}
417
418	#[rstest]
419	#[tokio::test]
420	async fn test_cache_backend_wrapper_delete_and_exists() {
421		// Arrange
422		let cache = Arc::new(InMemoryCache::new());
423		let backend = CacheSessionBackend::new(cache);
424		let data = json!({"item": "to_delete"});
425
426		// Act - save and verify exists
427		backend.save("wrap_del", &data, Some(3600)).await.unwrap();
428		assert!(backend.exists("wrap_del").await.unwrap());
429
430		// Act - delete
431		backend.delete("wrap_del").await.unwrap();
432
433		// Assert
434		assert!(!backend.exists("wrap_del").await.unwrap());
435		let loaded: Option<serde_json::Value> = backend.load("wrap_del").await.unwrap();
436		assert!(loaded.is_none());
437	}
438}