106 lines
2.9 KiB
Rust
106 lines
2.9 KiB
Rust
//! 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<HashMap<String, SessionEntry>>,
|
|
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<AuthContext> {
|
|
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<String> {
|
|
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(())
|
|
}
|
|
}
|