dmsc/auth/
session.rs

1//! Copyright © 2025 Wenze Wei. All Rights Reserved.
2//! 
3//! This file is part of DMSC.
4//! The DMSC 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 DMSC.
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 = DMSCSessionManager::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 tokio::sync::RwLock;
68use uuid::Uuid;
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 DMSCSession {
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 DMSCSession {
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 DMSCSession {
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 `DMSCSession`
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 DMSCSessionManager {
229    /// Hash map of active sessions indexed by session ID
230    sessions: RwLock<HashMap<String, DMSCSession>>,
231    /// Default session timeout duration in seconds
232    timeout_secs: u64,
233    /// Maximum number of concurrent sessions allowed per user
234    max_sessions_per_user: usize,
235}
236
237impl DMSCSessionManager {
238    /// Creates a new session manager with the specified timeout.
239    ///
240    /// # Parameters
241    ///
242    /// - `timeout_secs`: Default session timeout in seconds
243    ///
244    /// # Returns
245    ///
246    /// A new instance of `DMSCSessionManager`
247    ///
248    /// # Notes
249    ///
250    /// Default maximum sessions per user is 5
251    pub fn new(timeout_secs: u64) -> Self {
252        Self {
253            sessions: RwLock::new(HashMap::new()),
254            timeout_secs,
255            max_sessions_per_user: 5,
256        }
257    }
258
259    /// Creates a new session for a user.
260    /// 
261    /// # Parameters
262    /// - `user_id`: ID of the user to create the session for
263    /// - `ip_address`: Optional client IP address
264    /// - `user_agent`: Optional client user agent
265    /// 
266    /// # Returns
267    /// The ID of the newly created session
268    /// 
269    /// # Notes
270    /// - If the user has reached the maximum number of sessions, the oldest session is removed
271    pub async fn create_session(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> crate::core::DMSCResult<String> {
272        let mut sessions = self.sessions.write().await;
273        
274        // Check if user has too many sessions
275        let user_sessions: Vec<String> = sessions.values()
276            .filter(|s| s.user_id == user_id && !s.is_expired())
277            .map(|s| s.id.clone())
278            .collect();
279        
280        if user_sessions.len() >= self.max_sessions_per_user {
281            // Remove oldest session
282            if let Some(oldest_id) = user_sessions.iter().min() {
283                sessions.remove(oldest_id);
284            }
285        }
286
287        // Create new session
288        let session = DMSCSession::new(user_id, self.timeout_secs, ip_address, user_agent);
289        let session_id = session.id.clone();
290        sessions.insert(session_id.clone(), session);
291        
292        Ok(session_id)
293    }
294
295    /// Gets a session by ID.
296    /// 
297    /// # Parameters
298    /// - `session_id`: ID of the session to retrieve
299    /// 
300    /// # Returns
301    /// `Some(DMSCSession)` if the session exists and is not expired, otherwise `None`
302    /// 
303    /// # Notes
304    /// - Expired sessions are automatically removed and return `None`
305    /// - The session's last accessed time is updated when retrieved
306    pub async fn get_session(&self, session_id: &str) -> crate::core::DMSCResult<Option<DMSCSession>> {
307        let mut sessions = self.sessions.write().await;
308        
309        if let Some(session) = sessions.get_mut(session_id) {
310            if session.is_expired() {
311                sessions.remove(session_id);
312                Ok(None)
313            } else {
314                session.touch();
315                Ok(Some(session.clone()))
316            }
317        } else {
318            Ok(None)
319        }
320    }
321
322    /// Updates a session's data.
323    /// 
324    /// # Parameters
325    /// - `session_id`: ID of the session to update
326    /// - `data`: HashMap of key-value pairs to update in the session
327    /// 
328    /// # Returns
329    /// `true` if the session was updated successfully, `false` if the session doesn't exist or is expired
330    /// 
331    /// # Notes
332    /// - The session's last accessed time is updated when modified
333    pub async fn update_session(&self, session_id: &str, data: HashMap<String, String>) -> crate::core::DMSCResult<bool> {
334        let mut sessions = self.sessions.write().await;
335        
336        if let Some(session) = sessions.get_mut(session_id) {
337            if session.is_expired() {
338                sessions.remove(session_id);
339                Ok(false)
340            } else {
341                for (key, value) in data {
342                    session.set_data(key, value);
343                }
344                session.touch();
345                Ok(true)
346            }
347        } else {
348            Ok(false)
349        }
350    }
351
352    /// Extends a session's expiration time.
353    /// 
354    /// # Parameters
355    /// - `session_id`: ID of the session to extend
356    /// 
357    /// # Returns
358    /// `true` if the session was extended successfully, `false` if the session doesn't exist or is expired
359    pub async fn extend_session(&self, session_id: &str) -> crate::core::DMSCResult<bool> {
360        let mut sessions = self.sessions.write().await;
361        
362        if let Some(session) = sessions.get_mut(session_id) {
363            if session.is_expired() {
364                sessions.remove(session_id);
365                Ok(false)
366            } else {
367                session.extend(self.timeout_secs);
368                Ok(true)
369            }
370        } else {
371            Ok(false)
372        }
373    }
374
375    /// Destroys a session by ID.
376    /// 
377    /// # Parameters
378    /// - `session_id`: ID of the session to destroy
379    /// 
380    /// # Returns
381    /// `true` if the session was destroyed successfully, `false` if the session doesn't exist
382    pub async fn destroy_session(&self, session_id: &str) -> crate::core::DMSCResult<bool> {
383        let mut sessions = self.sessions.write().await;
384        Ok(sessions.remove(session_id).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::DMSCResult<usize> {
395        let mut sessions = self.sessions.write().await;
396        let mut count = 0;
397        
398        sessions.retain(|_, session| {
399            if session.user_id == user_id {
400                count += 1;
401                false
402            } else {
403                true
404            }
405        });
406        
407        Ok(count)
408    }
409
410    /// Gets all active sessions for a user.
411    /// 
412    /// # Parameters
413    /// - `user_id`: ID of the user whose sessions to retrieve
414    /// 
415    /// # Returns
416    /// A vector of active sessions for the user
417    pub async fn get_user_sessions(&self, user_id: &str) -> crate::core::DMSCResult<Vec<DMSCSession>> {
418        let sessions = self.sessions.read().await;
419        
420        let user_sessions: Vec<DMSCSession> = sessions.values()
421            .filter(|s| s.user_id == user_id && !s.is_expired())
422            .cloned()
423            .collect();
424        
425        Ok(user_sessions)
426    }
427
428    /// Cleans up all expired sessions.
429    /// 
430    /// # Returns
431    /// The number of expired sessions cleaned up
432    pub async fn cleanup_expired(&self) -> crate::core::DMSCResult<usize> {
433        let mut sessions = self.sessions.write().await;
434        let mut count = 0;
435        
436        sessions.retain(|_, session| {
437            if session.is_expired() {
438                count += 1;
439                false
440            } else {
441                true
442            }
443        });
444        
445        Ok(count)
446    }
447
448    /// Cleans up all sessions.
449    /// 
450    /// This method removes all sessions, regardless of their expiration status.
451    pub async fn cleanup_all(&self) -> crate::core::DMSCResult<()> {
452        let mut sessions = self.sessions.write().await;
453        sessions.clear();
454        Ok(())
455    }
456
457    /// Gets the default session timeout.
458    /// 
459    /// # Returns
460    /// The default session timeout in seconds
461    pub fn get_timeout(&self) -> u64 {
462        self.timeout_secs
463    }
464
465    /// Sets the default session timeout.
466    /// 
467    /// # Parameters
468    /// - `timeout_secs`: New default session timeout in seconds
469    pub fn set_timeout(&mut self, timeout_secs: u64) {
470        self.timeout_secs = timeout_secs;
471    }
472}
473
474#[cfg(feature = "pyo3")]
475/// Python bindings for the Session Manager.
476///
477/// This module provides Python interface to DMSC session management functionality,
478/// enabling Python applications to manage user sessions with expiration and data storage.
479///
480/// ## Supported Operations
481///
482/// - Session creation with user ID, IP address, and user agent tracking
483/// - Session retrieval and validation
484/// - Session data storage with key-value pairs
485/// - Session expiration management
486/// - Session cleanup for expired sessions
487///
488/// ## Python Usage Example
489///
490/// ```python
491/// from dmsc import DMSCSessionManager
492///
493/// # Create session manager with 30-minute timeout
494/// session_manager = DMSCSessionManager(1800)
495///
496/// # Create a new session
497/// session_id = session_manager.create_session(
498///     "user123",
499///     "192.168.1.1",
500///     "Mozilla/5.0"
501/// )
502///
503/// # Get session data
504/// session = session_manager.get_session(session_id)
505/// if session:
506///     print(f"Session created at: {session.created_at}")
507///     print(f"Session expires at: {session.expires_at}")
508///
509/// # Update session data
510/// session_manager.update_session(session_id, {"theme": "dark"})
511///
512/// # Extend session
513/// session_manager.extend_session(session_id)
514///
515/// # Destroy session when done
516/// session_manager.destroy_session(session_id)
517/// ```
518///
519/// ## Limitations
520///
521/// The current Python bindings do not support async session operations.
522/// For async scenarios, use the Rust API directly or implement async wrappers
523/// using Python's asyncio library.
524#[pyo3::prelude::pymethods]
525impl DMSCSessionManager {
526    #[new]
527    fn py_new(timeout_secs: u64) -> PyResult<Self> {
528        Ok(Self::new(timeout_secs))
529    }
530    
531    #[pyo3(name = "create_session")]
532    fn create_session_impl(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> PyResult<String> {
533        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
534        rt.block_on(async {
535            self.create_session(user_id, ip_address, user_agent).await
536                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
537        })
538    }
539    
540    #[pyo3(name = "get_session")]
541    fn get_session_impl(&self, session_id: String) -> PyResult<Option<DMSCSession>> {
542        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
543        rt.block_on(async {
544            self.get_session(&session_id).await
545                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
546        })
547    }
548    
549    #[pyo3(name = "update_session")]
550    fn update_session_impl(&self, session_id: String, data: HashMap<String, String>) -> PyResult<bool> {
551        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
552        rt.block_on(async {
553            self.update_session(&session_id, data).await
554                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
555        })
556    }
557    
558    #[pyo3(name = "destroy_session")]
559    fn destroy_session_impl(&self, session_id: String) -> PyResult<bool> {
560        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
561        rt.block_on(async {
562            self.destroy_session(&session_id).await
563                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
564        })
565    }
566    
567    #[pyo3(name = "extend_session")]
568    fn extend_session_impl(&self, session_id: String) -> PyResult<bool> {
569        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
570        rt.block_on(async {
571            self.extend_session(&session_id).await
572                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
573        })
574    }
575    
576    #[pyo3(name = "cleanup_expired")]
577    fn cleanup_expired_impl(&self) -> PyResult<usize> {
578        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
579        rt.block_on(async {
580            self.cleanup_expired().await
581                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
582        })
583    }
584    
585    #[pyo3(name = "get_timeout")]
586    fn get_timeout_impl(&self) -> u64 {
587        self.get_timeout()
588    }
589}