Archived
feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user