Skip to main content

ri/auth/
session.rs

1//! Copyright © 2025 Wenze Wei. All Rights Reserved.
2//! 
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//! 
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//! 
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//! 
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! Session management implementation for Ri.
19//! 
20//! This module provides session management functionality, including:
21//! - Session creation and validation
22//! - Session expiration tracking
23//! - User session management
24//! - Session data storage
25//! - Expired session cleanup
26//! 
27//! # Design Principles
28//! - **Security**: Session IDs are generated using UUID v4 for uniqueness
29//! - **Performance**: Efficient session lookup with hash maps
30//! - **Flexibility**: Supports custom session timeout and data storage
31//! - **Scalability**: Limits sessions per user to prevent resource exhaustion
32//! - **Convenience**: Automatically cleans up expired sessions
33//! - **Thread Safety**: Uses RwLock for concurrent access to session storage
34//! 
35//! # Usage Examples
36//! ```rust
37//! // Create a session manager with 30-minute timeout
38//! let session_manager = RiSessionManager::new(1800);
39//! 
40//! // Create a new session for a user
41//! let session_id = session_manager.create_session(
42//!     "user123".to_string(),
43//!     Some("192.168.1.1".to_string()),
44//!     Some("Mozilla/5.0".to_string())
45//! ).await?;
46//! 
47//! // Get session data
48//! let session = session_manager.get_session(&session_id).await?;
49//! 
50//! // Update session data
51//! let mut data = HashMap::new();
52//! data.insert("theme".to_string(), "dark".to_string());
53//! session_manager.update_session(&session_id, data).await?;
54//! 
55//! // Destroy a session
56//! session_manager.destroy_session(&session_id).await?;
57//! 
58//! // Cleanup expired sessions
59//! let cleaned_count = session_manager.cleanup_expired().await?;
60//! ```
61
62#![allow(non_snake_case)]
63
64use serde::{Deserialize, Serialize};
65use std::collections::HashMap;
66use std::time::{SystemTime, UNIX_EPOCH};
67use uuid::Uuid;
68use crate::core::concurrent::RiShardedLock;
69
70#[cfg(feature = "pyo3")]
71use pyo3::PyResult;
72
73/// Session structure for tracking user sessions.
74///
75/// This struct represents a user session with metadata, expiration tracking,
76/// and custom data storage. Sessions are uniquely identified by UUIDs.
77#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct RiSession {
80    /// Unique session identifier generated using UUID v4
81    pub id: String,
82    /// User ID associated with this session
83    pub user_id: String,
84    /// Session creation time as Unix timestamp
85    pub created_at: u64,
86    /// Last access time as Unix timestamp (updated on each access)
87    pub last_accessed: u64,
88    /// Session expiration time as Unix timestamp
89    pub expires_at: u64,
90    /// Custom key-value data associated with the session
91    pub data: HashMap<String, String>,
92    /// Client IP address from which the session was created
93    pub ip_address: Option<String>,
94    /// Client user agent string from which the session was created
95    pub user_agent: Option<String>,
96}
97
98#[cfg(feature = "pyo3")]
99#[pyo3::prelude::pymethods]
100impl RiSession {
101    #[new]
102    fn py_new(
103        id: Option<String>,
104        user_id: String,
105        created_at: Option<u64>,
106        last_accessed: Option<u64>,
107        expires_at: Option<u64>,
108        data: Option<HashMap<String, String>>,
109        ip_address: Option<String>,
110        user_agent: Option<String>,
111    ) -> Self {
112        let now = SystemTime::now()
113            .duration_since(UNIX_EPOCH)
114            .map_or(0, |d| d.as_secs());
115        
116        Self {
117            id: id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
118            user_id,
119            created_at: created_at.unwrap_or(now),
120            last_accessed: last_accessed.unwrap_or(now),
121            expires_at: expires_at.unwrap_or(now + 28800),
122            data: data.unwrap_or_default(),
123            ip_address,
124            user_agent,
125        }
126    }
127}
128
129impl RiSession {
130    /// Creates a new session for a user.
131    /// 
132    /// # Parameters
133    /// - `user_id`: ID of the user to create the session for
134    /// - `timeout_secs`: Session timeout in seconds
135    /// - `ip_address`: Optional client IP address
136    /// - `user_agent`: Optional client user agent
137    /// 
138    /// # Returns
139    /// A new instance of `RiSession`
140    pub fn new(user_id: String, timeout_secs: u64, ip_address: Option<String>, user_agent: Option<String>) -> Self {
141        let now = SystemTime::now()
142            .duration_since(UNIX_EPOCH)
143            .map_or(0, |d| d.as_secs());
144
145        Self {
146            id: Uuid::new_v4().to_string(),
147            user_id,
148            created_at: now,
149            last_accessed: now,
150            expires_at: now + timeout_secs,
151            data: HashMap::new(),
152            ip_address,
153            user_agent,
154        }
155    }
156
157    /// Checks if the session has expired.
158    /// 
159    /// # Returns
160    /// `true` if the session has expired, otherwise `false`
161    pub fn is_expired(&self) -> bool {
162        let now = SystemTime::now()
163            .duration_since(UNIX_EPOCH)
164            .map_or(0, |d| d.as_secs());
165        now > self.expires_at
166    }
167
168    /// Updates the last accessed time of the session.
169    /// 
170    /// This method is called when a session is accessed to update its
171    /// last accessed timestamp, which can be used for session activity tracking.
172    pub fn touch(&mut self) {
173        let now = SystemTime::now()
174            .duration_since(UNIX_EPOCH)
175            .unwrap()
176            .as_secs();
177        self.last_accessed = now;
178    }
179
180    /// Extends the session expiration time.
181    /// 
182    /// # Parameters
183    /// - `timeout_secs`: New timeout in seconds from the current time
184    pub fn extend(&mut self, timeout_secs: u64) {
185        let now = SystemTime::now()
186            .duration_since(UNIX_EPOCH)
187            .map_or(0, |d| d.as_secs());
188        self.expires_at = now + timeout_secs;
189    }
190
191    /// Gets a value from the session data.
192    /// 
193    /// # Parameters
194    /// - `key`: Key to look up in the session data
195    /// 
196    /// # Returns
197    /// `Some(&String)` if the key exists, otherwise `None`
198    pub fn get_data(&self, key: &str) -> Option<&String> {
199        self.data.get(key)
200    }
201
202    /// Sets a value in the session data.
203    /// 
204    /// # Parameters
205    /// - `key`: Key to set in the session data
206    /// - `value`: Value to associate with the key
207    pub fn set_data(&mut self, key: String, value: String) {
208        self.data.insert(key, value);
209    }
210
211    /// Removes a value from the session data.
212    /// 
213    /// # Parameters
214    /// - `key`: Key to remove from the session data
215    /// 
216    /// # Returns
217    /// `Some(String)` if the key existed and was removed, otherwise `None`
218    pub fn remove_data(&mut self, key: &str) -> Option<String> {
219        self.data.remove(key)
220    }
221}
222
223/// Session manager for handling user sessions.
224///
225/// This struct manages session creation, validation, and cleanup. It limits
226/// the number of sessions per user and automatically cleans up expired sessions.
227#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
228pub struct RiSessionManager {
229    sessions: RiShardedLock<String, RiSession>,
230    timeout_secs: u64,
231    max_sessions_per_user: usize,
232}
233
234impl RiSessionManager {
235    /// Creates a new session manager with the specified timeout.
236    ///
237    /// # Parameters
238    ///
239    /// - `timeout_secs`: Default session timeout in seconds
240    ///
241    /// # Returns
242    ///
243    /// A new instance of `RiSessionManager`
244    ///
245    /// # Notes
246    ///
247    /// Default maximum sessions per user is 5
248    pub fn new(timeout_secs: u64) -> Self {
249        Self {
250            sessions: RiShardedLock::with_default_shards(),
251            timeout_secs,
252            max_sessions_per_user: 5,
253        }
254    }
255
256    /// Creates a new session for a user.
257    /// 
258    /// # Parameters
259    /// - `user_id`: ID of the user to create the session for
260    /// - `ip_address`: Optional client IP address
261    /// - `user_agent`: Optional client user agent
262    /// 
263    /// # Returns
264    /// The ID of the newly created session
265    /// 
266    /// # Notes
267    /// - If the user has reached the maximum number of sessions, the oldest session is removed
268    pub async fn create_session(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> crate::core::RiResult<String> {
269        let user_sessions: Vec<(String, u64)> = self.sessions.collect_where(|_, s| s.user_id == user_id && !s.is_expired()).await
270            .into_iter()
271            .map(|s| (s.id.clone(), s.created_at))
272            .collect();
273        
274        if user_sessions.len() >= self.max_sessions_per_user {
275            let mut sessions_with_time = user_sessions;
276            sessions_with_time.sort_by_key(|(_, t)| *t);
277            
278            if let Some((oldest_id, _)) = sessions_with_time.first() {
279                self.sessions.remove(oldest_id).await;
280            }
281        }
282
283        let session = RiSession::new(user_id, self.timeout_secs, ip_address, user_agent);
284        let session_id = session.id.clone();
285        self.sessions.insert(session_id.clone(), session).await;
286        
287        Ok(session_id)
288    }
289
290    /// Gets a session by ID.
291    /// 
292    /// # Parameters
293    /// - `session_id`: ID of the session to retrieve
294    /// 
295    /// # Returns
296    /// `Some(RiSession)` if the session exists and is not expired, otherwise `None`
297    /// 
298    /// # Notes
299    /// - Expired sessions are automatically removed and return `None`
300    /// - The session's last accessed time is updated when retrieved
301    pub async fn get_session(&self, session_id: &str) -> crate::core::RiResult<Option<RiSession>> {
302        let session = self.sessions.get(session_id).await;
303        
304        match session {
305            Some(mut s) => {
306                if s.is_expired() {
307                    self.sessions.remove(session_id).await;
308                    Ok(None)
309                } else {
310                    s.touch();
311                    self.sessions.insert(session_id.to_string(), s.clone()).await;
312                    Ok(Some(s))
313                }
314            }
315            None => Ok(None),
316        }
317    }
318
319    /// Updates a session's data.
320    /// 
321    /// # Parameters
322    /// - `session_id`: ID of the session to update
323    /// - `data`: HashMap of key-value pairs to update in the session
324    /// 
325    /// # Returns
326    /// `true` if the session was updated successfully, `false` if the session doesn't exist or is expired
327    /// 
328    /// # Notes
329    /// - The session's last accessed time is updated when modified
330    pub async fn update_session(&self, session_id: &str, data: HashMap<String, String>) -> crate::core::RiResult<bool> {
331        let session = self.sessions.get(session_id).await;
332        
333        match session {
334            Some(mut s) => {
335                if s.is_expired() {
336                    self.sessions.remove(session_id).await;
337                    Ok(false)
338                } else {
339                    for (key, value) in data {
340                        s.set_data(key, value);
341                    }
342                    s.touch();
343                    self.sessions.insert(session_id.to_string(), s).await;
344                    Ok(true)
345                }
346            }
347            None => Ok(false),
348        }
349    }
350
351    /// Extends a session's expiration time.
352    /// 
353    /// # Parameters
354    /// - `session_id`: ID of the session to extend
355    /// 
356    /// # Returns
357    /// `true` if the session was extended successfully, `false` if the session doesn't exist or is expired
358    pub async fn extend_session(&self, session_id: &str) -> crate::core::RiResult<bool> {
359        let session = self.sessions.get(session_id).await;
360        
361        match session {
362            Some(mut s) => {
363                if s.is_expired() {
364                    self.sessions.remove(session_id).await;
365                    Ok(false)
366                } else {
367                    s.extend(self.timeout_secs);
368                    self.sessions.insert(session_id.to_string(), s).await;
369                    Ok(true)
370                }
371            }
372            None => Ok(false),
373        }
374    }
375
376    /// Destroys a session by ID.
377    /// 
378    /// # Parameters
379    /// - `session_id`: ID of the session to destroy
380    /// 
381    /// # Returns
382    /// `true` if the session was destroyed successfully, `false` if the session doesn't exist
383    pub async fn destroy_session(&self, session_id: &str) -> crate::core::RiResult<bool> {
384        Ok(self.sessions.remove(session_id).await.is_some())
385    }
386
387    /// Destroys all sessions for a user.
388    /// 
389    /// # Parameters
390    /// - `user_id`: ID of the user whose sessions to destroy
391    /// 
392    /// # Returns
393    /// The number of sessions destroyed
394    pub async fn destroy_user_sessions(&self, user_id: &str) -> crate::core::RiResult<usize> {
395        let count = self.sessions.remove_where(|_, s| s.user_id == user_id).await;
396        Ok(count)
397    }
398
399    /// Gets all active sessions for a user.
400    /// 
401    /// # Parameters
402    /// - `user_id`: ID of the user whose sessions to retrieve
403    /// 
404    /// # Returns
405    /// A vector of active sessions for the user
406    pub async fn get_user_sessions(&self, user_id: &str) -> crate::core::RiResult<Vec<RiSession>> {
407        let user_sessions = self.sessions.collect_where(|_, s| s.user_id == user_id && !s.is_expired()).await;
408        Ok(user_sessions)
409    }
410
411    /// Cleans up all expired sessions.
412    /// 
413    /// # Returns
414    /// The number of expired sessions cleaned up
415    pub async fn cleanup_expired(&self) -> crate::core::RiResult<usize> {
416        let count = self.sessions.remove_where(|_, s| s.is_expired()).await;
417        Ok(count)
418    }
419
420    /// Cleans up all sessions.
421    /// 
422    /// This method removes all sessions, regardless of their expiration status.
423    pub async fn cleanup_all(&self) -> crate::core::RiResult<()> {
424        self.sessions.clear().await;
425        Ok(())
426    }
427
428    /// Gets the default session timeout.
429    /// 
430    /// # Returns
431    /// The default session timeout in seconds
432    pub fn get_timeout(&self) -> u64 {
433        self.timeout_secs
434    }
435
436    /// Sets the default session timeout.
437    /// 
438    /// # Parameters
439    /// - `timeout_secs`: New default session timeout in seconds
440    pub fn set_timeout(&mut self, timeout_secs: u64) {
441        self.timeout_secs = timeout_secs;
442    }
443}
444
445#[cfg(feature = "pyo3")]
446/// Python bindings for the Session Manager.
447///
448/// This module provides Python interface to Ri session management functionality,
449/// enabling Python applications to manage user sessions with expiration and data storage.
450///
451/// ## Supported Operations
452///
453/// - Session creation with user ID, IP address, and user agent tracking
454/// - Session retrieval and validation
455/// - Session data storage with key-value pairs
456/// - Session expiration management
457/// - Session cleanup for expired sessions
458///
459/// ## Python Usage Example
460///
461/// ```python
462/// from ri import RiSessionManager
463///
464/// # Create session manager with 30-minute timeout
465/// session_manager = RiSessionManager(1800)
466///
467/// # Create a new session
468/// session_id = session_manager.create_session(
469///     "user123",
470///     "192.168.1.1",
471///     "Mozilla/5.0"
472/// )
473///
474/// # Get session data
475/// session = session_manager.get_session(session_id)
476/// if session:
477///     print(f"Session created at: {session.created_at}")
478///     print(f"Session expires at: {session.expires_at}")
479///
480/// # Update session data
481/// session_manager.update_session(session_id, {"theme": "dark"})
482///
483/// # Extend session
484/// session_manager.extend_session(session_id)
485///
486/// # Destroy session when done
487/// session_manager.destroy_session(session_id)
488/// ```
489///
490/// ## Limitations
491///
492/// The current Python bindings do not support async session operations.
493/// For async scenarios, use the Rust API directly or implement async wrappers
494/// using Python's asyncio library.
495#[pyo3::prelude::pymethods]
496impl RiSessionManager {
497    #[new]
498    fn py_new(timeout_secs: u64) -> PyResult<Self> {
499        Ok(Self::new(timeout_secs))
500    }
501    
502    #[pyo3(name = "create_session")]
503    fn create_session_impl(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> PyResult<String> {
504        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
505        rt.block_on(async {
506            self.create_session(user_id, ip_address, user_agent).await
507                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
508        })
509    }
510    
511    #[pyo3(name = "get_session")]
512    fn get_session_impl(&self, session_id: String) -> PyResult<Option<RiSession>> {
513        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
514        rt.block_on(async {
515            self.get_session(&session_id).await
516                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
517        })
518    }
519    
520    #[pyo3(name = "update_session")]
521    fn update_session_impl(&self, session_id: String, data: HashMap<String, String>) -> PyResult<bool> {
522        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
523        rt.block_on(async {
524            self.update_session(&session_id, data).await
525                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
526        })
527    }
528    
529    #[pyo3(name = "destroy_session")]
530    fn destroy_session_impl(&self, session_id: String) -> PyResult<bool> {
531        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
532        rt.block_on(async {
533            self.destroy_session(&session_id).await
534                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
535        })
536    }
537    
538    #[pyo3(name = "extend_session")]
539    fn extend_session_impl(&self, session_id: String) -> PyResult<bool> {
540        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
541        rt.block_on(async {
542            self.extend_session(&session_id).await
543                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
544        })
545    }
546    
547    #[pyo3(name = "cleanup_expired")]
548    fn cleanup_expired_impl(&self) -> PyResult<usize> {
549        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
550        rt.block_on(async {
551            self.cleanup_expired().await
552                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
553        })
554    }
555    
556    #[pyo3(name = "get_timeout")]
557    fn get_timeout_impl(&self) -> u64 {
558        self.get_timeout()
559    }
560}