axum_jwt_sessions/storage/
mod.rs

1use crate::error::Result;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[cfg(feature = "cloudflare-kv")]
6pub mod cloudflare_kv;
7
8#[cfg(feature = "cloudflare-kv")]
9pub use cloudflare_kv::CloudflareKvStorage;
10
11/// Data structure representing a session within a user's session array
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
14pub struct SessionData {
15    pub session_id: Uuid,
16    pub expires_at: OffsetDateTime,
17}
18
19/// Trait for session storage backends.
20///
21/// With the JWT-based session data approach, the storage is now responsible
22/// for managing refresh tokens organized by user_id. Each user can have multiple
23/// active sessions stored as an array. Session data is stored directly in the JWT access token.
24///
25/// All methods are async and return Send futures, allowing them to be used
26/// safely across thread boundaries in async contexts.
27///
28/// # Example Implementation
29///
30/// ```rust
31/// use std::collections::HashMap;
32/// use std::sync::Arc;
33/// use tokio::sync::RwLock;
34/// use time::OffsetDateTime;
35/// use uuid::Uuid;
36/// use axum_jwt_sessions::error::Result;
37/// use axum_jwt_sessions::{SessionData, SessionStorage};
38///
39/// struct UserSessionStorage {
40///     user_sessions: Arc<RwLock<HashMap<String, Vec<SessionData>>>>,
41/// }
42///
43/// impl SessionStorage for UserSessionStorage {
44///     async fn create_user_session(&self, user_id: String, session_id: Uuid, expires_at: OffsetDateTime) -> Result<()> {
45///         let mut sessions = self.user_sessions.write().await;
46///         let user_sessions = sessions.entry(user_id).or_insert_with(Vec::new);
47///
48///         user_sessions.push(SessionData {
49///             session_id,
50///             expires_at,
51///         });
52///
53///         Ok(())
54///     }
55///
56///     async fn get_user_sessions(&self, user_id: &str) -> Result<Vec<SessionData>> {
57///         let sessions = self.user_sessions.read().await;
58///         let now = OffsetDateTime::now_utc();
59///
60///         if let Some(user_sessions) = sessions.get(user_id) {
61///             // Return only non-expired sessions
62///             let active_sessions: Vec<SessionData> = user_sessions
63///                 .iter()
64///                 .filter(|session| session.expires_at > now)
65///                 .cloned()
66///                 .collect();
67///             Ok(active_sessions)
68///         } else {
69///             Ok(Vec::new())
70///         }
71///     }
72///
73///     async fn revoke_user_session(&self, user_id: &str, session_id: &Uuid) -> Result<()> {
74///         let mut sessions = self.user_sessions.write().await;
75///         if let Some(user_sessions) = sessions.get_mut(user_id) {
76///             user_sessions.retain(|session| session.session_id != *session_id);
77///             if user_sessions.is_empty() {
78///                 sessions.remove(user_id);
79///             }
80///         }
81///         Ok(())
82///     }
83///
84///     async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<()> {
85///         self.user_sessions.write().await.remove(user_id);
86///         Ok(())
87///     }
88/// }
89/// ```
90pub trait SessionStorage: Send + Sync {
91    /// Create a new session for a user
92    fn create_user_session(
93        &self,
94        user_id: String,
95        session_id: Uuid,
96        expires_at: OffsetDateTime,
97    ) -> impl std::future::Future<Output = Result<()>> + Send;
98
99    /// Get all active sessions for a user (excludes expired sessions)
100    fn get_user_sessions(
101        &self,
102        user_id: &str,
103    ) -> impl std::future::Future<Output = Result<Vec<SessionData>>> + Send;
104
105    /// Revoke a specific session for a user
106    fn revoke_user_session(
107        &self,
108        user_id: &str,
109        session_id: &Uuid,
110    ) -> impl std::future::Future<Output = Result<()>> + Send;
111
112    /// Revoke all sessions for a user
113    fn revoke_all_user_sessions(
114        &self,
115        user_id: &str,
116    ) -> impl std::future::Future<Output = Result<()>> + Send;
117
118    /// Check if a specific session exists for a user (not revoked or expired)
119    fn user_session_exists(
120        &self,
121        user_id: &str,
122        session_id: &Uuid,
123    ) -> impl std::future::Future<Output = Result<bool>> + Send {
124        async move {
125            let sessions = self.get_user_sessions(user_id).await?;
126            Ok(sessions
127                .iter()
128                .any(|session| session.session_id == *session_id))
129        }
130    }
131
132    /// Get session data for a specific session_id within a user's sessions
133    fn get_session_data(
134        &self,
135        user_id: &str,
136        session_id: &Uuid,
137    ) -> impl std::future::Future<Output = Result<Option<SessionData>>> + Send {
138        async move {
139            let sessions = self.get_user_sessions(user_id).await?;
140            Ok(sessions
141                .into_iter()
142                .find(|session| session.session_id == *session_id))
143        }
144    }
145
146    /// Clean up expired sessions for a user
147    fn cleanup_expired_sessions(
148        &self,
149        user_id: &str,
150    ) -> impl std::future::Future<Output = Result<()>> + Send {
151        async move {
152            let sessions = self.get_user_sessions(user_id).await?;
153            let now = OffsetDateTime::now_utc();
154
155            // Remove expired sessions
156            for session in sessions {
157                if session.expires_at <= now {
158                    self.revoke_user_session(user_id, &session.session_id)
159                        .await?;
160                }
161            }
162
163            Ok(())
164        }
165    }
166}