pub mod encryption;
pub mod storage;
#[cfg(target_arch = "wasm32")]
pub mod wasm;
#[cfg(not(target_arch = "wasm32"))]
pub mod native;
#[cfg(feature = "session-management")]
use crate::auth::Session;
#[cfg(feature = "session-management")]
use crate::error::{Error, Result};
#[cfg(feature = "session-management")]
use chrono::{DateTime, Utc};
#[cfg(feature = "session-management")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "session-management")]
use std::collections::HashMap;
#[cfg(feature = "session-management")]
use std::sync::Arc;
#[cfg(feature = "session-management")]
use uuid::Uuid;
#[cfg(all(feature = "session-management", feature = "parking_lot"))]
use parking_lot::{Mutex, RwLock};
#[cfg(all(feature = "session-management", not(feature = "parking_lot")))]
use std::sync::{Mutex, RwLock};
#[cfg(feature = "session-management")]
use storage::StorageBackend;
#[cfg(feature = "session-management")]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait SessionStorage: Send + Sync {
async fn store_session(
&self,
key: &str,
session: &SessionData,
expires_at: Option<DateTime<Utc>>,
) -> Result<()>;
async fn get_session(&self, key: &str) -> Result<Option<SessionData>>;
async fn remove_session(&self, key: &str) -> Result<()>;
async fn clear_all_sessions(&self) -> Result<()>;
async fn list_session_keys(&self) -> Result<Vec<String>>;
fn is_available(&self) -> bool;
}
#[cfg(feature = "session-management")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionData {
pub session: Session,
pub metadata: SessionMetadata,
pub platform_data: HashMap<String, serde_json::Value>,
}
#[cfg(feature = "session-management")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMetadata {
pub session_id: Uuid,
pub device_id: Option<String>,
pub client_id: Option<String>,
pub created_at: DateTime<Utc>,
pub last_accessed_at: DateTime<Utc>,
pub last_refreshed_at: Option<DateTime<Utc>>,
pub source: SessionSource,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub location: Option<SessionLocation>,
pub tags: Vec<String>,
pub custom: HashMap<String, serde_json::Value>,
}
#[cfg(feature = "session-management")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SessionSource {
Web { tab_id: Option<String> },
Mobile { app_version: Option<String> },
Desktop { app_version: Option<String> },
Server { service: Option<String> },
Cli { tool_name: Option<String> },
Other { description: String },
}
#[cfg(feature = "session-management")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionLocation {
pub country: Option<String>,
pub city: Option<String>,
pub region: Option<String>,
pub timezone: Option<String>,
pub coordinates: Option<(f64, f64)>, }
#[cfg(feature = "session-management")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SessionEvent {
Created { session_id: Uuid },
Updated {
session_id: Uuid,
changes: Vec<String>,
},
Accessed {
session_id: Uuid,
timestamp: DateTime<Utc>,
},
Refreshed {
session_id: Uuid,
timestamp: DateTime<Utc>,
},
Expired {
session_id: Uuid,
timestamp: DateTime<Utc>,
},
Destroyed { session_id: Uuid, reason: String },
CrossTabSync {
session_id: Uuid,
source_tab: String,
},
Conflict {
session_id: Uuid,
conflict_type: String,
},
}
#[cfg(feature = "session-management")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossTabMessage {
pub message_id: Uuid,
pub session_id: Uuid,
pub event_type: String,
pub payload: serde_json::Value,
pub timestamp: DateTime<Utc>,
pub source_tab: String,
}
#[cfg(feature = "session-management")]
#[derive(Debug, Clone)]
pub struct SessionManagerConfig {
pub storage_backend: Arc<StorageBackend>,
pub enable_cross_tab_sync: bool,
pub session_key_prefix: String,
pub default_expiry_seconds: i64,
pub enable_encryption: bool,
pub encryption_key: Option<[u8; 32]>,
pub enable_monitoring: bool,
pub max_memory_sessions: usize,
pub sync_interval_seconds: u64,
}
#[cfg(feature = "session-management")]
pub struct SessionManager {
config: SessionManagerConfig,
active_sessions: Arc<RwLock<HashMap<Uuid, SessionData>>>,
event_listeners: Arc<RwLock<HashMap<Uuid, SessionEventCallback>>>,
cross_tab_channel: Arc<Mutex<Option<Box<dyn CrossTabChannel>>>>,
}
#[cfg(feature = "session-management")]
pub type SessionEventCallback = Box<dyn Fn(SessionEvent) + Send + Sync + 'static>;
#[cfg(feature = "session-management")]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait CrossTabChannel: Send + Sync {
async fn send_message(&self, message: CrossTabMessage) -> Result<()>;
fn on_message(&self, callback: Box<dyn Fn(CrossTabMessage) + Send + Sync>);
async fn close(&self) -> Result<()>;
}
#[cfg(feature = "session-management")]
impl SessionManager {
pub fn new(config: SessionManagerConfig) -> Self {
Self {
config,
active_sessions: Arc::new(RwLock::new(HashMap::new())),
event_listeners: Arc::new(RwLock::new(HashMap::new())),
cross_tab_channel: Arc::new(Mutex::new(None)),
}
}
pub async fn initialize(&self) -> Result<()> {
self.load_persisted_sessions().await?;
if self.config.enable_cross_tab_sync {
self.setup_cross_tab_sync().await?;
}
self.start_background_tasks().await?;
Ok(())
}
pub async fn store_session(&self, session: Session) -> Result<Uuid> {
let session_id = Uuid::new_v4();
let now = Utc::now();
let metadata = SessionMetadata {
session_id,
device_id: self.detect_device_id(),
client_id: self.detect_client_id(),
created_at: now,
last_accessed_at: now,
last_refreshed_at: None,
source: self.detect_session_source(),
ip_address: None, user_agent: self.detect_user_agent(),
location: None, tags: Vec::new(),
custom: HashMap::new(),
};
let session_data = SessionData {
session,
metadata,
platform_data: HashMap::new(),
};
{
let mut sessions = self.active_sessions.write();
sessions.insert(session_id, session_data.clone());
}
let key = format!("{}{}", self.config.session_key_prefix, session_id);
let expires_at = Some(session_data.session.expires_at);
self.config
.storage_backend
.store_session(&key, &session_data, expires_at)
.await?;
self.emit_session_event(SessionEvent::Created { session_id });
if self.config.enable_cross_tab_sync {
self.sync_to_other_tabs(session_id, "session_created")
.await?;
}
Ok(session_id)
}
pub async fn get_session(&self, session_id: Uuid) -> Result<Option<SessionData>> {
{
let sessions = self.active_sessions.read();
if let Some(session_data) = sessions.get(&session_id) {
let mut updated_data = session_data.clone();
updated_data.metadata.last_accessed_at = Utc::now();
drop(sessions);
let mut sessions = self.active_sessions.write();
sessions.insert(session_id, updated_data.clone());
self.emit_session_event(SessionEvent::Accessed {
session_id,
timestamp: Utc::now(),
});
return Ok(Some(updated_data));
}
}
let key = format!("{}{}", self.config.session_key_prefix, session_id);
if let Some(mut session_data) = self.config.storage_backend.get_session(&key).await? {
session_data.metadata.last_accessed_at = Utc::now();
{
let mut sessions = self.active_sessions.write();
sessions.insert(session_id, session_data.clone());
}
self.emit_session_event(SessionEvent::Accessed {
session_id,
timestamp: Utc::now(),
});
Ok(Some(session_data))
} else {
Ok(None)
}
}
pub async fn update_session(&self, session_id: Uuid, updated_session: Session) -> Result<()> {
let mut changes = Vec::new();
if let Some(mut session_data) = self.get_session(session_id).await? {
if session_data.session.access_token != updated_session.access_token {
changes.push("access_token".to_string());
}
if session_data.session.refresh_token != updated_session.refresh_token {
changes.push("refresh_token".to_string());
}
if session_data.session.expires_at != updated_session.expires_at {
changes.push("expires_at".to_string());
}
session_data.session = updated_session;
session_data.metadata.last_accessed_at = Utc::now();
if changes.contains(&"access_token".to_string())
|| changes.contains(&"refresh_token".to_string())
{
session_data.metadata.last_refreshed_at = Some(Utc::now());
}
{
let mut sessions = self.active_sessions.write();
sessions.insert(session_id, session_data.clone());
}
let key = format!("{}{}", self.config.session_key_prefix, session_id);
let expires_at = Some(session_data.session.expires_at);
self.config
.storage_backend
.store_session(&key, &session_data, expires_at)
.await?;
self.emit_session_event(SessionEvent::Updated {
session_id,
changes,
});
if self.config.enable_cross_tab_sync {
self.sync_to_other_tabs(session_id, "session_updated")
.await?;
}
} else {
return Err(Error::auth(format!("Session {} not found", session_id)));
}
Ok(())
}
pub async fn remove_session(&self, session_id: Uuid, reason: String) -> Result<()> {
{
let mut sessions = self.active_sessions.write();
sessions.remove(&session_id);
}
let key = format!("{}{}", self.config.session_key_prefix, session_id);
self.config.storage_backend.remove_session(&key).await?;
self.emit_session_event(SessionEvent::Destroyed { session_id, reason });
if self.config.enable_cross_tab_sync {
self.sync_to_other_tabs(session_id, "session_destroyed")
.await?;
}
Ok(())
}
pub async fn list_sessions(&self) -> Result<Vec<SessionData>> {
let sessions = self.active_sessions.read();
Ok(sessions.values().cloned().collect())
}
pub fn on_session_event<F>(&self, callback: F) -> Uuid
where
F: Fn(SessionEvent) + Send + Sync + 'static,
{
let listener_id = Uuid::new_v4();
let mut listeners = self.event_listeners.write();
listeners.insert(listener_id, Box::new(callback));
listener_id
}
pub fn remove_event_listener(&self, listener_id: Uuid) {
let mut listeners = self.event_listeners.write();
listeners.remove(&listener_id);
}
async fn load_persisted_sessions(&self) -> Result<()> {
let keys = self.config.storage_backend.list_session_keys().await?;
let mut valid_sessions = Vec::new();
let mut expired_keys = Vec::new();
for key in keys {
if let Some(session_data) = self.config.storage_backend.get_session(&key).await? {
if session_data.session.expires_at > Utc::now() {
if let Ok(uuid) = key
.strip_prefix(&self.config.session_key_prefix)
.unwrap_or(&key)
.parse::<Uuid>()
{
valid_sessions.push((uuid, session_data));
}
} else {
expired_keys.push(key);
}
}
}
{
let mut sessions = self.active_sessions.write();
for (uuid, session_data) in valid_sessions {
sessions.insert(uuid, session_data);
}
}
for key in expired_keys {
let _ = self.config.storage_backend.remove_session(&key).await;
}
Ok(())
}
async fn setup_cross_tab_sync(&self) -> Result<()> {
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
{
let channel = crate::session::wasm::WasmCrossTabChannel::new()?;
let mut cross_tab = self.cross_tab_channel.lock();
*cross_tab = Some(Box::new(channel));
Ok(())
}
#[cfg(all(target_arch = "wasm32", not(feature = "wasm")))]
{
Err(Error::platform("Cross-tab sync requires 'wasm' feature"))
}
#[cfg(not(target_arch = "wasm32"))]
{
let channel = crate::session::native::NativeCrossTabChannel::new()?;
let mut cross_tab = self.cross_tab_channel.lock();
*cross_tab = Some(Box::new(channel));
Ok(())
}
}
async fn start_background_tasks(&self) -> Result<()> {
Ok(())
}
#[allow(clippy::await_holding_lock)]
async fn sync_to_other_tabs(&self, session_id: Uuid, event_type: &str) -> Result<()> {
if let Some(channel) = self.cross_tab_channel.lock().as_ref() {
let message = CrossTabMessage {
message_id: Uuid::new_v4(),
session_id,
event_type: event_type.to_string(),
payload: serde_json::json!({}),
timestamp: Utc::now(),
source_tab: self
.detect_tab_id()
.unwrap_or_else(|| "unknown".to_string()),
};
channel.send_message(message).await?;
}
Ok(())
}
fn emit_session_event(&self, event: SessionEvent) {
let listeners = self.event_listeners.read();
for callback in listeners.values() {
callback(event.clone());
}
}
fn detect_device_id(&self) -> Option<String> {
None
}
fn detect_client_id(&self) -> Option<String> {
None
}
fn detect_tab_id(&self) -> Option<String> {
None
}
fn detect_session_source(&self) -> SessionSource {
#[cfg(target_arch = "wasm32")]
{
SessionSource::Web {
tab_id: self.detect_tab_id(),
}
}
#[cfg(not(target_arch = "wasm32"))]
{
SessionSource::Desktop { app_version: None }
}
}
fn detect_user_agent(&self) -> Option<String> {
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
{
web_sys::window().and_then(|w| w.navigator().user_agent().ok())
}
#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
{
None
}
}
}
#[cfg(feature = "session-management")]
impl Default for SessionManagerConfig {
fn default() -> Self {
Self {
storage_backend: Arc::new(StorageBackend::Memory(
crate::session::storage::MemoryStorage::new(),
)),
enable_cross_tab_sync: true,
session_key_prefix: "supabase_session_".to_string(),
default_expiry_seconds: 3600, enable_encryption: false,
encryption_key: None,
enable_monitoring: true,
max_memory_sessions: 100,
sync_interval_seconds: 30,
}
}
}