Archived
121 lines
4.0 KiB
Rust
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)
|
|
}
|
|
}
|