//! Identity graph nodes — User, Role, Scope, OAuthToken, Session as //! strongly-typed Engram node structs. //! //! Each node maps to an Engram graph node. The identity graph looks like: //! //! ```text //! User ──has_role──▶ Role ──grants──▶ Scope //! │ //! └──has_session──▶ Session ──authenticated_via──▶ OAuthToken //! ``` use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; // ── User ────────────────────────────────────────────────────────────────────── /// A user identity node in the Engram graph. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct User { /// Stable UUID for this user. pub id: Uuid, /// Email address — unique identifier for the user. pub email: String, /// Display name (may differ from email). pub display_name: String, /// When this user node was created. pub created_at: DateTime, } impl User { /// Create a new user with a fresh UUID. pub fn new(email: impl Into, display_name: impl Into) -> Self { Self { id: Uuid::new_v4(), email: email.into(), display_name: display_name.into(), created_at: Utc::now(), } } /// Deserialize from a serde_json::Value (as stored in Engram). pub fn from_value(value: &serde_json::Value) -> Option { serde_json::from_value(value.clone()).ok() } /// Serialize to serde_json::Value for storage in Engram. pub fn to_value(&self) -> serde_json::Value { serde_json::to_value(self).expect("User is always serializable") } } // ── Role ────────────────────────────────────────────────────────────────────── /// A role node — grants a set of named permissions to connected User nodes. /// /// Edge: `User ──has_role──▶ Role` #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Role { pub id: Uuid, pub name: String, /// Flat list of permission strings (e.g., `"orders:read"`, `"users:write"`). pub permissions: Vec, } impl Role { pub fn new(name: impl Into) -> Self { Self { id: Uuid::new_v4(), name: name.into(), permissions: Vec::new(), } } pub fn with_permission(mut self, perm: impl Into) -> Self { self.permissions.push(perm.into()); self } pub fn with_permissions(mut self, perms: impl IntoIterator>) -> Self { self.permissions.extend(perms.into_iter().map(|p| p.into())); self } pub fn has_permission(&self, perm: &str) -> bool { self.permissions.iter().any(|p| p == perm) } pub fn from_value(value: &serde_json::Value) -> Option { serde_json::from_value(value.clone()).ok() } pub fn to_value(&self) -> serde_json::Value { serde_json::to_value(self).expect("Role is always serializable") } } // ── Scope ───────────────────────────────────────────────────────────────────── /// An OAuth scope node. /// /// Edge: `Role ──grants──▶ Scope` #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Scope { pub id: Uuid, /// The OAuth scope string (e.g., `"openid"`, `"email"`, `"profile"`). pub name: String, pub description: String, } impl Scope { pub fn new(name: impl Into, description: impl Into) -> Self { Self { id: Uuid::new_v4(), name: name.into(), description: description.into(), } } pub fn from_value(value: &serde_json::Value) -> Option { serde_json::from_value(value.clone()).ok() } pub fn to_value(&self) -> serde_json::Value { serde_json::to_value(self).expect("Scope is always serializable") } } // ── OAuthToken ──────────────────────────────────────────────────────────────── /// An OAuth token node — stores hashed tokens so the graph is breach-safe. /// /// Edge: `Session ──authenticated_via──▶ OAuthToken` /// /// Tokens are hashed with SHA-256 before storage. The raw token is never /// persisted — only the hash. Refresh tokens use the same scheme. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct OAuthToken { pub id: Uuid, pub provider: String, /// SHA-256 hex hash of the access token. pub access_token_hash: String, /// SHA-256 hex hash of the refresh token, if present. pub refresh_token_hash: Option, pub expires_at: DateTime, /// Scopes granted by this token. pub scopes: Vec, } impl OAuthToken { pub fn new( provider: impl Into, access_token_hash: impl Into, refresh_token_hash: Option, expires_at: DateTime, scopes: Vec, ) -> Self { Self { id: Uuid::new_v4(), provider: provider.into(), access_token_hash: access_token_hash.into(), refresh_token_hash, expires_at, scopes, } } pub fn is_expired(&self) -> bool { Utc::now() >= self.expires_at } pub fn has_scope(&self, scope: &str) -> bool { self.scopes.iter().any(|s| s == scope) } pub fn from_value(value: &serde_json::Value) -> Option { serde_json::from_value(value.clone()).ok() } pub fn to_value(&self) -> serde_json::Value { serde_json::to_value(self).expect("OAuthToken is always serializable") } } // ── Session ─────────────────────────────────────────────────────────────────── /// A session node — represents an active authenticated session. /// /// Edge: `User ──has_session──▶ Session` #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Session { pub id: Uuid, /// The user this session belongs to. pub user_id: Uuid, pub created_at: DateTime, pub expires_at: DateTime, /// The IP address that created this session. pub ip_address: Option, } impl Session { pub fn new(user_id: Uuid, ttl_seconds: i64, ip_address: Option) -> Self { let now = Utc::now(); let expires_at = now + chrono::Duration::seconds(ttl_seconds); Self { id: Uuid::new_v4(), user_id, created_at: now, expires_at, ip_address, } } pub fn is_expired(&self) -> bool { Utc::now() >= self.expires_at } pub fn from_value(value: &serde_json::Value) -> Option { serde_json::from_value(value.clone()).ok() } pub fn to_value(&self) -> serde_json::Value { serde_json::to_value(self).expect("Session is always serializable") } } // ── Edge type constants ─────────────────────────────────────────────────────── /// Edge type: User → Role pub const EDGE_HAS_ROLE: &str = "has_role"; /// Edge type: User → Session pub const EDGE_HAS_SESSION: &str = "has_session"; /// Edge type: Session → OAuthToken pub const EDGE_AUTHENTICATED_VIA: &str = "authenticated_via"; /// Edge type: Role → Scope pub const EDGE_GRANTS: &str = "grants"; // ── Node type constants ─────────────────────────────────────────────────────── pub const NODE_USER: &str = "User"; pub const NODE_ROLE: &str = "Role"; pub const NODE_SCOPE: &str = "Scope"; pub const NODE_OAUTH_TOKEN: &str = "OAuthToken"; pub const NODE_SESSION: &str = "Session";