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}