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}