use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{SystemTime, UNIX_EPOCH};
use super::auth_context::AuthContext;
struct CachedSession {
auth_context: AuthContext,
created_at: u64,
expires_at: u64,
}
pub struct SessionHandleStore {
sessions: RwLock<HashMap<String, CachedSession>>,
default_ttl_secs: u64,
}
impl SessionHandleStore {
pub fn new(default_ttl_secs: u64) -> Self {
Self {
sessions: RwLock::new(HashMap::new()),
default_ttl_secs,
}
}
pub fn create(&self, auth_context: AuthContext) -> String {
let now = now_secs();
let handle = generate_handle();
let cached = CachedSession {
auth_context,
created_at: now,
expires_at: now + self.default_ttl_secs,
};
let mut sessions = self.sessions.write().unwrap_or_else(|p| p.into_inner());
sessions.insert(handle.clone(), cached);
let expired: Vec<String> = sessions
.iter()
.filter(|(_, s)| now >= s.expires_at)
.take(100)
.map(|(k, _)| k.clone())
.collect();
for key in expired {
sessions.remove(&key);
}
handle
}
pub fn resolve(&self, handle: &str) -> Option<AuthContext> {
let sessions = self.sessions.read().unwrap_or_else(|p| p.into_inner());
let cached = sessions.get(handle)?;
let now = now_secs();
if now >= cached.expires_at {
return None; }
Some(cached.auth_context.clone())
}
pub fn invalidate(&self, handle: &str) -> bool {
let mut sessions = self.sessions.write().unwrap_or_else(|p| p.into_inner());
sessions.remove(handle).is_some()
}
pub fn count(&self) -> usize {
let now = now_secs();
let sessions = self.sessions.read().unwrap_or_else(|p| p.into_inner());
sessions.values().filter(|s| now < s.expires_at).count()
}
pub fn oldest_age_secs(&self) -> u64 {
let now = now_secs();
let sessions = self.sessions.read().unwrap_or_else(|p| p.into_inner());
sessions
.values()
.filter(|s| now < s.expires_at)
.map(|s| now.saturating_sub(s.created_at))
.max()
.unwrap_or(0)
}
}
impl Default for SessionHandleStore {
fn default() -> Self {
Self::new(3600)
}
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn generate_handle() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let ts = now_secs();
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("nds_{ts:x}_{seq:08x}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::control::security::auth_context::AuthContext;
use crate::control::security::identity::{AuthMethod, AuthenticatedIdentity, Role};
use crate::types::TenantId;
fn test_auth_context() -> AuthContext {
let identity = AuthenticatedIdentity {
user_id: 42,
username: "alice".into(),
tenant_id: TenantId::new(1),
auth_method: AuthMethod::ApiKey,
roles: vec![Role::ReadWrite],
is_superuser: false,
};
AuthContext::from_identity(
&identity,
crate::control::security::auth_context::generate_session_id(),
)
}
#[test]
fn create_and_resolve() {
let store = SessionHandleStore::new(3600);
let handle = store.create(test_auth_context());
assert!(handle.starts_with("nds_"));
let resolved = store.resolve(&handle).unwrap();
assert_eq!(resolved.username, "alice");
}
#[test]
fn expired_handle_returns_none() {
let store = SessionHandleStore::new(0); let handle = store.create(test_auth_context());
assert!(store.resolve(&handle).is_none());
}
#[test]
fn invalidate_removes_handle() {
let store = SessionHandleStore::new(3600);
let handle = store.create(test_auth_context());
assert!(store.resolve(&handle).is_some());
store.invalidate(&handle);
assert!(store.resolve(&handle).is_none());
}
#[test]
fn unknown_handle_returns_none() {
let store = SessionHandleStore::new(3600);
assert!(store.resolve("nds_nonexistent").is_none());
}
}