Archived
f4abfe6fdc
Belated rename commit for foundation/el-ui — was missed in the workspace-wide crates→vessels pass earlier today. Same structural intent as the rename in the other repos: 'crates' is the Rust word, 'vessel' is El's, and the directory rename is the marker that this slot holds an El buildable unit even if its current contents are still Rust pending port. Plus the El ports themselves — manifest.el + src/main.el per sub- vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout, el-platform, el-publish, el-secrets, el-services, el-style, el-ui- compiler). The ui-compiler is a stub: elc only emits C right now; generating browser-target JS/Wasm is the biggest open language gap and gets its own project. Until then, el-ui-compiler emits a JS module that throws elc.backend_missing so callers fail loudly. Cross-repo path dependencies in Cargo.toml updated to vessels/.
106 lines
2.9 KiB
Rust
106 lines
2.9 KiB
Rust
//! Session provider — server-side sessions stored in memory.
|
|
//!
|
|
//! In production, sessions are stored in Redis or Engram (configured via
|
|
//! `session_store = "redis"` or `session_store = "engram"` in `manifest.el`).
|
|
//! This implementation uses in-memory storage for simplicity and testing.
|
|
|
|
use crate::{AuthContext, AuthError, AuthProvider, AuthResult, AuthUser, RoleRegistry};
|
|
use std::{
|
|
collections::HashMap,
|
|
sync::Mutex,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
struct SessionEntry {
|
|
context: AuthContext,
|
|
created_at: Instant,
|
|
ttl: Duration,
|
|
}
|
|
|
|
impl SessionEntry {
|
|
fn is_expired(&self) -> bool {
|
|
self.created_at.elapsed() > self.ttl
|
|
}
|
|
}
|
|
|
|
/// In-memory session store.
|
|
pub struct SessionProvider {
|
|
sessions: Mutex<HashMap<String, SessionEntry>>,
|
|
pub ttl: Duration,
|
|
}
|
|
|
|
impl SessionProvider {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
sessions: Mutex::new(HashMap::new()),
|
|
ttl: Duration::from_secs(3600),
|
|
}
|
|
}
|
|
|
|
pub fn with_ttl(mut self, seconds: u64) -> Self {
|
|
self.ttl = Duration::from_secs(seconds);
|
|
self
|
|
}
|
|
|
|
fn generate_session_id() -> String {
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.subsec_nanos())
|
|
.unwrap_or(0);
|
|
format!("sess-{:016x}", nanos as u64 ^ 0x7b5e3f1a2c4d6890)
|
|
}
|
|
|
|
/// Count active (non-expired) sessions.
|
|
pub fn active_session_count(&self) -> usize {
|
|
let sessions = self.sessions.lock().expect("session lock poisoned");
|
|
sessions.values().filter(|s| !s.is_expired()).count()
|
|
}
|
|
}
|
|
|
|
impl Default for SessionProvider {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl AuthProvider for SessionProvider {
|
|
fn name(&self) -> &'static str {
|
|
"session"
|
|
}
|
|
|
|
fn verify(&self, session_id: &str) -> AuthResult<AuthContext> {
|
|
let mut sessions = self.sessions.lock().expect("session lock poisoned");
|
|
// Clean expired sessions
|
|
sessions.retain(|_, v| !v.is_expired());
|
|
sessions
|
|
.get(session_id)
|
|
.filter(|s| !s.is_expired())
|
|
.map(|s| s.context.clone())
|
|
.ok_or(AuthError::SessionNotFound)
|
|
}
|
|
|
|
fn issue(&self, user: AuthUser, _role_registry: &RoleRegistry) -> AuthResult<String> {
|
|
let session_id = Self::generate_session_id();
|
|
let ctx = AuthContext::authenticated(user, Vec::new(), &session_id);
|
|
let entry = SessionEntry {
|
|
context: ctx,
|
|
created_at: Instant::now(),
|
|
ttl: self.ttl,
|
|
};
|
|
self.sessions
|
|
.lock()
|
|
.expect("session lock poisoned")
|
|
.insert(session_id.clone(), entry);
|
|
Ok(session_id)
|
|
}
|
|
|
|
fn revoke(&self, session_id: &str) -> AuthResult<()> {
|
|
self.sessions
|
|
.lock()
|
|
.expect("session lock poisoned")
|
|
.remove(session_id);
|
|
Ok(())
|
|
}
|
|
}
|