This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/ui/vessels/el-identity/src/session.rs
T

121 lines
4.0 KiB
Rust

//! Session management — sessions are Engram graph nodes connected to User nodes.
//!
//! Session lifecycle:
//! 1. `SessionManager::create()` → Session node + `User ──has_session──▶ Session` edge
//! 2. `SessionManager::validate()` → find Session node, check expiry
//! 3. `SessionManager::invalidate()` → delete Session node (edges auto-removed)
//!
//! The `SessionManager` works exclusively through the `EngramClient` trait — no
//! in-memory map, no cache. The graph is the source of truth.
use crate::{
engram::EngramClient,
error::{IdentityError, IdentityResult},
nodes::{Session, EDGE_HAS_SESSION, NODE_SESSION},
};
use std::sync::Arc;
/// Manages session nodes in the Engram identity graph.
pub struct SessionManager {
client: Arc<dyn EngramClient>,
/// Default session TTL in seconds (default: 3600 = 1 hour).
pub default_ttl_seconds: i64,
}
impl SessionManager {
pub fn new(client: Arc<dyn EngramClient>) -> Self {
Self {
client,
default_ttl_seconds: 3600,
}
}
pub fn with_ttl(mut self, seconds: i64) -> Self {
self.default_ttl_seconds = seconds;
self
}
/// Create a new session for the given user.
///
/// Stores the Session node in Engram and creates a `has_session` edge
/// from the User node to the Session node.
///
/// Returns the session ID string.
pub fn create(
&self,
user_id: uuid::Uuid,
ip_address: Option<String>,
) -> IdentityResult<Session> {
let session = Session::new(user_id, self.default_ttl_seconds, ip_address);
let session_id_str = session.id.to_string();
// Store session node
self.client
.create_node(NODE_SESSION, session.to_value())
.map_err(|e| IdentityError::GraphError(e.to_string()))?;
// Edge: User → Session (has_session)
self.client
.create_edge(&user_id.to_string(), &session_id_str, EDGE_HAS_SESSION)
.map_err(|e| IdentityError::GraphError(e.to_string()))?;
Ok(session)
}
/// Validate a session by ID.
///
/// Returns the `Session` node if found and not expired.
/// Automatically deletes expired sessions on lookup (lazy expiry).
pub fn validate(&self, session_id: &str) -> IdentityResult<Session> {
let node = self
.client
.get_node(session_id)
.map_err(|e| IdentityError::GraphError(e.to_string()))?
.ok_or(IdentityError::SessionNotFound)?;
let session = Session::from_value(&node)
.ok_or_else(|| IdentityError::GraphError("session node parse failed".into()))?;
if session.is_expired() {
// Lazy cleanup — remove expired session from graph
let _ = self.client.delete_node(session_id);
return Err(IdentityError::SessionExpired);
}
Ok(session)
}
/// Invalidate (delete) a session node from the graph.
pub fn invalidate(&self, session_id: &str) -> IdentityResult<()> {
self.client
.delete_node(session_id)
.map_err(|e| IdentityError::GraphError(e.to_string()))
}
/// List all sessions for a user by traversing `has_session` edges.
pub fn list_for_user(&self, user_id: &str) -> IdentityResult<Vec<Session>> {
let nodes = self
.client
.find_connected(user_id, EDGE_HAS_SESSION)
.map_err(|e| IdentityError::GraphError(e.to_string()))?;
let sessions: Vec<Session> = nodes
.iter()
.filter_map(Session::from_value)
.filter(|s| !s.is_expired())
.collect();
Ok(sessions)
}
/// Invalidate all sessions for a user (logout everywhere).
pub fn invalidate_all_for_user(&self, user_id: &str) -> IdentityResult<usize> {
let sessions = self.list_for_user(user_id)?;
let count = sessions.len();
for session in &sessions {
let _ = self.client.delete_node(&session.id.to_string());
}
Ok(count)
}
}