Some checks failed
CI/CD Pipeline / unit-tests (push) Failing after 1m16s
CI/CD Pipeline / integration-tests (push) Failing after 2m32s
CI/CD Pipeline / lint (push) Successful in 5m22s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped
200 lines
7.2 KiB
Rust
200 lines
7.2 KiB
Rust
```rust
|
|
1: //! Distributed session management using Redis
|
|
2: //!
|
|
3: //! This module provides session storage that works across multiple proxy nodes.
|
|
4: //! Sessions are stored in Redis and can be accessed by any proxy instance.
|
|
5:
|
|
6: use common::{CacheLayer, CacheError, CacheResult, SessionData};
|
|
7: use uuid::Uuid;
|
|
8: use chrono::{DateTime, Utc, Duration};
|
|
9:
|
|
10: /// Session manager for distributed auth sessions
|
|
11: #[derive(Clone)]
|
|
12: pub struct SessionManager {
|
|
13: cache: CacheLayer,
|
|
14: session_ttl: u64, // Session TTL in seconds
|
|
15: }
|
|
16:
|
|
17: impl SessionManager {
|
|
18: /// Create a new session manager
|
|
19: pub fn new(cache: CacheLayer, session_ttl: u64) -> Self {
|
|
20: Self { cache, session_ttl }
|
|
21: }
|
|
22:
|
|
23: /// Create a new session for a user
|
|
24: pub async fn create_session(
|
|
25: &self,
|
|
26: user_id: Uuid,
|
|
27: email: String,
|
|
28: role: String,
|
|
29: ) -> CacheResult<String> {
|
|
30: let session_token = Uuid::new_v4().to_string();
|
|
31: let now = Utc::now();
|
|
32: let expires_at = now + Duration::seconds(self.session_ttl as i64);
|
|
33:
|
|
34: let session = SessionData {
|
|
35: user_id,
|
|
36: email,
|
|
37: role,
|
|
38: created_at: now,
|
|
39: expires_at,
|
|
40: };
|
|
41:
|
|
42: // Store session in Redis
|
|
43: let key = format!("session:{}", session_token);
|
|
44: self.cache.set(&key, &session).await?;
|
|
45:
|
|
46: // Also add to user's active sessions set (for multi-device logout)
|
|
47: let user_sessions_key = format!("user:{}:sessions", user_id);
|
|
48: if let Some(redis) = &self.cache.redis {
|
|
49: let mut conn = redis.get_async_connection().await?;
|
|
50: redis::cmd("SADD")
|
|
51: .arg(&user_sessions_key)
|
|
52: .arg(&session_token)
|
|
53: .query_async(&mut conn)
|
|
54: .await?;
|
|
55:
|
|
56: // Set expiration on the set
|
|
57: redis::cmd("EXPIRE")
|
|
58: .arg(&user_sessions_key)
|
|
59: .arg(self.session_ttl * 2)
|
|
60: .query_async(&mut conn)
|
|
61: .await?;
|
|
62: }
|
|
63:
|
|
64: Ok(session_token)
|
|
65: }
|
|
66:
|
|
67: /// Get a session by token
|
|
68: pub async fn get_session(&self, session_token: &str) -> CacheResult<Option<SessionData>> {
|
|
69: self.cache.get_session(session_token.to_string()).await
|
|
70: }
|
|
71:
|
|
72: /// Validate a session (check if it exists and is not expired)
|
|
73: pub async fn validate_session(&self, session_token: &str) -> CacheResult<Option<SessionData>> {
|
|
74: let session = self.get_session(session_token).await?;
|
|
75:
|
|
76: if let Some(session) = session {
|
|
77: let now = Utc::now();
|
|
78: if now < session.expires_at {
|
|
79: return Ok(Some(session));
|
|
80: }
|
|
81: }
|
|
82:
|
|
83: Ok(None)
|
|
84: }
|
|
85:
|
|
86: /// Refresh a session (extend expiration)
|
|
87: pub async fn refresh_session(&self, session_token: &str) -> CacheResult<bool> {
|
|
88: if let Some(mut session) = self.get_session(session_token).await? {
|
|
89: let now = Utc::now();
|
|
90: session.expires_at = now + Duration::seconds(self.session_ttl as i64);
|
|
91:
|
|
92: let key = format!("session:{}", session_token);
|
|
93: self.cache.set(&key, &session).await?;
|
|
94: return Ok(true);
|
|
95: }
|
|
96:
|
|
97: Ok(false)
|
|
98: }
|
|
99:
|
|
100: /// Delete a session (logout)
|
|
101: pub async fn delete_session(&self, session_token: &str) -> CacheResult<()> {
|
|
102: // Get the session first to remove from user's session set
|
|
103: if let Some(session) = self.get_session(session_token).await? {
|
|
104: let user_sessions_key = format!("user:{}:sessions", session.user_id);
|
|
105:
|
|
106: if let Some(redis) = &self.cache.redis {
|
|
107: let mut conn = redis.get_async_connection().await?;
|
|
108: redis::cmd("SREM")
|
|
109: .arg(&user_sessions_key)
|
|
110: .arg(session_token)
|
|
111: .query_async(&mut conn)
|
|
112: .await?;
|
|
113: }
|
|
114: }
|
|
115:
|
|
116: self.cache.delete_session(session_token.to_string()).await
|
|
117: }
|
|
118:
|
|
119: /// Delete all sessions for a user (logout from all devices)
|
|
120: pub async fn delete_all_user_sessions(&self, user_id: Uuid) -> CacheResult<usize> {
|
|
121: let user_sessions_key = format!("user:{}:sessions", user_id);
|
|
122:
|
|
123: if let Some(redis) = &self.cache.redis {
|
|
124: let mut conn = redis.get_async_connection().await?;
|
|
125:
|
|
126: // Get all session tokens for this user
|
|
127: let session_tokens: Vec<String> = redis::cmd("SMEMBERS")
|
|
128: .arg(&user_sessions_key)
|
|
129: .query_async(&mut conn)
|
|
130: .await?;
|
|
131:
|
|
132: let count = session_tokens.len();
|
|
133:
|
|
134: // Delete each session
|
|
135: for token in &session_tokens {
|
|
136: let session_key = format!("session:{}", token);
|
|
137: redis::cmd("DEL")
|
|
138: .arg(&session_key)
|
|
139: .query_async(&mut conn)
|
|
140: .await?;
|
|
141: }
|
|
142:
|
|
143: // Delete the user's session set
|
|
144: redis::cmd("DEL")
|
|
145: .arg(&user_sessions_key)
|
|
146: .query_async(&mut conn)
|
|
147: .await?;
|
|
148:
|
|
149: Ok(count)
|
|
150: } else {
|
|
151: Ok(0)
|
|
152: }
|
|
153: }
|
|
154:
|
|
155: /// Get all active sessions for a user
|
|
156: pub async fn get_user_sessions(&self, user_id: Uuid) -> CacheResult<Vec<SessionData>> {
|
|
157: let user_sessions_key = format!("user:{}:sessions", user_id);
|
|
158:
|
|
159: if let Some(redis) = &self.cache.redis {
|
|
160: let mut conn = redis.get_async_connection().await?;
|
|
161:
|
|
162: let session_tokens: Vec<String> = redis::cmd("SMEMBERS")
|
|
163: .arg(&user_sessions_key)
|
|
164: .query_async(&mut conn)
|
|
165: .await?;
|
|
166:
|
|
167: let mut sessions = Vec::new();
|
|
168: for token in session_tokens {
|
|
169: if let Some(session) = self.get_session(&token).await? {
|
|
170: sessions.push(session);
|
|
171: }
|
|
172: }
|
|
173:
|
|
174: Ok(sessions)
|
|
175: } else {
|
|
176: Ok(vec![])
|
|
177: }
|
|
178: }
|
|
179:
|
|
180: /// Count active sessions for a user
|
|
181: pub async fn get_user_session_count(&self, user_id: Uuid) -> CacheResult<usize> {
|
|
182: let sessions = self.get_user_sessions(user_id).await?;
|
|
183: Ok(sessions.len())
|
|
184: }
|
|
185: }
|
|
186:
|
|
187: #[cfg(test)]
|
|
188: mod tests {
|
|
189: use super::*;
|
|
190:
|
|
191: #[tokio::test]
|
|
192: async fn test_session_manager_creation() {
|
|
193: let cache = CacheLayer::new(None, 3600);
|
|
194: let manager = SessionManager::new(cache, 3600);
|
|
195: assert_eq!(manager.session_ttl, 3600);
|
|
196: }
|
|
197: }
|
|
```
|