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}