//! Session provider — server-side sessions stored in memory. //! //! In production, sessions are stored in Redis or Engram (configured via //! `session_store = "redis"` or `session_store = "engram"` in `el.toml`). //! This implementation uses in-memory storage for simplicity and testing. use crate::{AuthContext, AuthError, AuthProvider, AuthResult, AuthUser, RoleRegistry}; use std::{ collections::HashMap, sync::Mutex, time::{Duration, Instant}, }; struct SessionEntry { context: AuthContext, created_at: Instant, ttl: Duration, } impl SessionEntry { fn is_expired(&self) -> bool { self.created_at.elapsed() > self.ttl } } /// In-memory session store. pub struct SessionProvider { sessions: Mutex>, pub ttl: Duration, } impl SessionProvider { pub fn new() -> Self { Self { sessions: Mutex::new(HashMap::new()), ttl: Duration::from_secs(3600), } } pub fn with_ttl(mut self, seconds: u64) -> Self { self.ttl = Duration::from_secs(seconds); self } fn generate_session_id() -> String { use std::time::{SystemTime, UNIX_EPOCH}; let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.subsec_nanos()) .unwrap_or(0); format!("sess-{:016x}", nanos as u64 ^ 0x7b5e3f1a2c4d6890) } /// Count active (non-expired) sessions. pub fn active_session_count(&self) -> usize { let sessions = self.sessions.lock().expect("session lock poisoned"); sessions.values().filter(|s| !s.is_expired()).count() } } impl Default for SessionProvider { fn default() -> Self { Self::new() } } impl AuthProvider for SessionProvider { fn name(&self) -> &'static str { "session" } fn verify(&self, session_id: &str) -> AuthResult { let mut sessions = self.sessions.lock().expect("session lock poisoned"); // Clean expired sessions sessions.retain(|_, v| !v.is_expired()); sessions .get(session_id) .filter(|s| !s.is_expired()) .map(|s| s.context.clone()) .ok_or(AuthError::SessionNotFound) } fn issue(&self, user: AuthUser, _role_registry: &RoleRegistry) -> AuthResult { let session_id = Self::generate_session_id(); let ctx = AuthContext::authenticated(user, Vec::new(), &session_id); let entry = SessionEntry { context: ctx, created_at: Instant::now(), ttl: self.ttl, }; self.sessions .lock() .expect("session lock poisoned") .insert(session_id.clone(), entry); Ok(session_id) } fn revoke(&self, session_id: &str) -> AuthResult<()> { self.sessions .lock() .expect("session lock poisoned") .remove(session_id); Ok(()) } }