//! 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, /// Default session TTL in seconds (default: 3600 = 1 hour). pub default_ttl_seconds: i64, } impl SessionManager { pub fn new(client: Arc) -> 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, ) -> IdentityResult { 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 { 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> { let nodes = self .client .find_connected(user_id, EDGE_HAS_SESSION) .map_err(|e| IdentityError::GraphError(e.to_string()))?; let sessions: Vec = 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 { 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) } }