supabase/session/
mod.rs

1//! Advanced Session Management for Supabase
2//!
3//! This module provides comprehensive session management functionality including:
4//! - Cross-tab session synchronization
5//! - Platform-aware session storage (localStorage/IndexedDB/filesystem)
6//! - Session encryption and secure storage
7//! - Real-time session monitoring and events
8//! - Offline session caching
9//! - Session state persistence
10
11pub mod encryption;
12pub mod storage;
13
14#[cfg(target_arch = "wasm32")]
15pub mod wasm;
16
17#[cfg(not(target_arch = "wasm32"))]
18pub mod native;
19
20#[cfg(feature = "session-management")]
21use crate::auth::Session;
22#[cfg(feature = "session-management")]
23use crate::error::{Error, Result};
24#[cfg(feature = "session-management")]
25use chrono::{DateTime, Utc};
26#[cfg(feature = "session-management")]
27use serde::{Deserialize, Serialize};
28#[cfg(feature = "session-management")]
29use std::collections::HashMap;
30#[cfg(feature = "session-management")]
31use std::sync::Arc;
32#[cfg(feature = "session-management")]
33use uuid::Uuid;
34
35// Import StorageBackend for enum-based storage
36
37#[cfg(all(feature = "session-management", feature = "parking_lot"))]
38use parking_lot::{Mutex, RwLock};
39#[cfg(all(feature = "session-management", not(feature = "parking_lot")))]
40use std::sync::{Mutex, RwLock};
41
42// Import storage backend enum
43#[cfg(feature = "session-management")]
44use storage::StorageBackend;
45
46/// Session storage backend trait for cross-platform implementation
47#[cfg(feature = "session-management")]
48#[async_trait::async_trait]
49pub trait SessionStorage: Send + Sync {
50    /// Store a session with optional expiry
51    async fn store_session(
52        &self,
53        key: &str,
54        session: &SessionData,
55        expires_at: Option<DateTime<Utc>>,
56    ) -> Result<()>;
57
58    /// Retrieve a session by key
59    async fn get_session(&self, key: &str) -> Result<Option<SessionData>>;
60
61    /// Remove a session by key
62    async fn remove_session(&self, key: &str) -> Result<()>;
63
64    /// Clear all sessions
65    async fn clear_all_sessions(&self) -> Result<()>;
66
67    /// List all session keys
68    async fn list_session_keys(&self) -> Result<Vec<String>>;
69
70    /// Check if storage is available
71    fn is_available(&self) -> bool;
72}
73
74/// Enhanced session data with metadata
75#[cfg(feature = "session-management")]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SessionData {
78    /// Core session information
79    pub session: Session,
80
81    /// Session metadata
82    pub metadata: SessionMetadata,
83
84    /// Platform-specific data
85    pub platform_data: HashMap<String, serde_json::Value>,
86}
87
88/// Session metadata for tracking and analytics
89#[cfg(feature = "session-management")]
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SessionMetadata {
92    /// Unique session identifier
93    pub session_id: Uuid,
94
95    /// Device identifier
96    pub device_id: Option<String>,
97
98    /// Browser/client identifier
99    pub client_id: Option<String>,
100
101    /// Session creation timestamp
102    pub created_at: DateTime<Utc>,
103
104    /// Last access timestamp
105    pub last_accessed_at: DateTime<Utc>,
106
107    /// Last refresh timestamp
108    pub last_refreshed_at: Option<DateTime<Utc>>,
109
110    /// Session source (web, mobile, desktop, etc.)
111    pub source: SessionSource,
112
113    /// IP address (if available)
114    pub ip_address: Option<String>,
115
116    /// User agent string
117    pub user_agent: Option<String>,
118
119    /// Geographic location info
120    pub location: Option<SessionLocation>,
121
122    /// Session tags for organization
123    pub tags: Vec<String>,
124
125    /// Custom metadata
126    pub custom: HashMap<String, serde_json::Value>,
127}
128
129/// Session source enumeration
130#[cfg(feature = "session-management")]
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub enum SessionSource {
133    /// Web browser session
134    Web { tab_id: Option<String> },
135    /// Mobile app session
136    Mobile { app_version: Option<String> },
137    /// Desktop app session
138    Desktop { app_version: Option<String> },
139    /// Server-side session
140    Server { service: Option<String> },
141    /// CLI tool session
142    Cli { tool_name: Option<String> },
143    /// Other/unknown session source
144    Other { description: String },
145}
146
147/// Geographic location information
148#[cfg(feature = "session-management")]
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SessionLocation {
151    pub country: Option<String>,
152    pub city: Option<String>,
153    pub region: Option<String>,
154    pub timezone: Option<String>,
155    pub coordinates: Option<(f64, f64)>, // (latitude, longitude)
156}
157
158/// Session event types for monitoring
159#[cfg(feature = "session-management")]
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum SessionEvent {
162    /// Session created
163    Created { session_id: Uuid },
164    /// Session updated
165    Updated {
166        session_id: Uuid,
167        changes: Vec<String>,
168    },
169    /// Session accessed
170    Accessed {
171        session_id: Uuid,
172        timestamp: DateTime<Utc>,
173    },
174    /// Session refreshed
175    Refreshed {
176        session_id: Uuid,
177        timestamp: DateTime<Utc>,
178    },
179    /// Session expired
180    Expired {
181        session_id: Uuid,
182        timestamp: DateTime<Utc>,
183    },
184    /// Session destroyed
185    Destroyed { session_id: Uuid, reason: String },
186    /// Cross-tab sync event
187    CrossTabSync {
188        session_id: Uuid,
189        source_tab: String,
190    },
191    /// Session conflict detected
192    Conflict {
193        session_id: Uuid,
194        conflict_type: String,
195    },
196}
197
198/// Cross-tab synchronization message
199#[cfg(feature = "session-management")]
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct CrossTabMessage {
202    pub message_id: Uuid,
203    pub session_id: Uuid,
204    pub event_type: String,
205    pub payload: serde_json::Value,
206    pub timestamp: DateTime<Utc>,
207    pub source_tab: String,
208}
209
210/// Session manager configuration
211#[cfg(feature = "session-management")]
212#[derive(Debug, Clone)]
213pub struct SessionManagerConfig {
214    /// Storage backend to use
215    pub storage_backend: Arc<StorageBackend>,
216
217    /// Enable cross-tab synchronization
218    pub enable_cross_tab_sync: bool,
219
220    /// Session key prefix for namespacing
221    pub session_key_prefix: String,
222
223    /// Default session expiry (in seconds)
224    pub default_expiry_seconds: i64,
225
226    /// Enable session encryption
227    pub enable_encryption: bool,
228
229    /// Encryption key (32 bytes)
230    pub encryption_key: Option<[u8; 32]>,
231
232    /// Enable session monitoring
233    pub enable_monitoring: bool,
234
235    /// Max number of sessions to keep in memory
236    pub max_memory_sessions: usize,
237
238    /// Background sync interval (in seconds)
239    pub sync_interval_seconds: u64,
240}
241
242/// Advanced Session Manager with cross-platform support
243#[cfg(feature = "session-management")]
244pub struct SessionManager {
245    config: SessionManagerConfig,
246    active_sessions: Arc<RwLock<HashMap<Uuid, SessionData>>>,
247    event_listeners: Arc<RwLock<HashMap<Uuid, SessionEventCallback>>>,
248    cross_tab_channel: Arc<Mutex<Option<Box<dyn CrossTabChannel>>>>,
249}
250
251/// Session event callback type
252#[cfg(feature = "session-management")]
253pub type SessionEventCallback = Box<dyn Fn(SessionEvent) + Send + Sync + 'static>;
254
255/// Cross-tab communication channel
256#[cfg(feature = "session-management")]
257#[async_trait::async_trait]
258pub trait CrossTabChannel: Send + Sync {
259    /// Send a message to other tabs
260    async fn send_message(&self, message: CrossTabMessage) -> Result<()>;
261
262    /// Register a message listener
263    fn on_message(&self, callback: Box<dyn Fn(CrossTabMessage) + Send + Sync>);
264
265    /// Close the channel
266    async fn close(&self) -> Result<()>;
267}
268
269#[cfg(feature = "session-management")]
270impl SessionManager {
271    /// Create a new session manager
272    pub fn new(config: SessionManagerConfig) -> Self {
273        Self {
274            config,
275            active_sessions: Arc::new(RwLock::new(HashMap::new())),
276            event_listeners: Arc::new(RwLock::new(HashMap::new())),
277            cross_tab_channel: Arc::new(Mutex::new(None)),
278        }
279    }
280
281    /// Initialize the session manager
282    pub async fn initialize(&self) -> Result<()> {
283        // Load persisted sessions
284        self.load_persisted_sessions().await?;
285
286        // Setup cross-tab sync if enabled
287        if self.config.enable_cross_tab_sync {
288            self.setup_cross_tab_sync().await?;
289        }
290
291        // Start background tasks
292        self.start_background_tasks().await?;
293
294        Ok(())
295    }
296
297    /// Store a session with advanced metadata
298    pub async fn store_session(&self, session: Session) -> Result<Uuid> {
299        let session_id = Uuid::new_v4();
300        let now = Utc::now();
301
302        let metadata = SessionMetadata {
303            session_id,
304            device_id: self.detect_device_id(),
305            client_id: self.detect_client_id(),
306            created_at: now,
307            last_accessed_at: now,
308            last_refreshed_at: None,
309            source: self.detect_session_source(),
310            ip_address: None, // TODO: Implement IP detection
311            user_agent: self.detect_user_agent(),
312            location: None, // TODO: Implement location detection
313            tags: Vec::new(),
314            custom: HashMap::new(),
315        };
316
317        let session_data = SessionData {
318            session,
319            metadata,
320            platform_data: HashMap::new(),
321        };
322
323        // Store in memory
324        {
325            let mut sessions = self.active_sessions.write();
326            sessions.insert(session_id, session_data.clone());
327        }
328
329        // Persist to storage
330        let key = format!("{}{}", self.config.session_key_prefix, session_id);
331        let expires_at = Some(session_data.session.expires_at);
332        self.config
333            .storage_backend
334            .store_session(&key, &session_data, expires_at)
335            .await?;
336
337        // Emit event
338        self.emit_session_event(SessionEvent::Created { session_id });
339
340        // Cross-tab sync
341        if self.config.enable_cross_tab_sync {
342            self.sync_to_other_tabs(session_id, "session_created")
343                .await?;
344        }
345
346        Ok(session_id)
347    }
348
349    /// Retrieve a session by ID
350    pub async fn get_session(&self, session_id: Uuid) -> Result<Option<SessionData>> {
351        // Check memory first
352        {
353            let sessions = self.active_sessions.read();
354            if let Some(session_data) = sessions.get(&session_id) {
355                // Update last accessed time
356                let mut updated_data = session_data.clone();
357                updated_data.metadata.last_accessed_at = Utc::now();
358
359                // Update in memory
360                drop(sessions);
361                let mut sessions = self.active_sessions.write();
362                sessions.insert(session_id, updated_data.clone());
363
364                // Emit access event
365                self.emit_session_event(SessionEvent::Accessed {
366                    session_id,
367                    timestamp: Utc::now(),
368                });
369
370                return Ok(Some(updated_data));
371            }
372        }
373
374        // Try storage if not in memory
375        let key = format!("{}{}", self.config.session_key_prefix, session_id);
376        if let Some(mut session_data) = self.config.storage_backend.get_session(&key).await? {
377            // Update access time
378            session_data.metadata.last_accessed_at = Utc::now();
379
380            // Store in memory
381            {
382                let mut sessions = self.active_sessions.write();
383                sessions.insert(session_id, session_data.clone());
384            }
385
386            // Emit access event
387            self.emit_session_event(SessionEvent::Accessed {
388                session_id,
389                timestamp: Utc::now(),
390            });
391
392            Ok(Some(session_data))
393        } else {
394            Ok(None)
395        }
396    }
397
398    /// Update a session
399    pub async fn update_session(&self, session_id: Uuid, updated_session: Session) -> Result<()> {
400        let mut changes = Vec::new();
401
402        // Get current session
403        if let Some(mut session_data) = self.get_session(session_id).await? {
404            // Track changes
405            if session_data.session.access_token != updated_session.access_token {
406                changes.push("access_token".to_string());
407            }
408            if session_data.session.refresh_token != updated_session.refresh_token {
409                changes.push("refresh_token".to_string());
410            }
411            if session_data.session.expires_at != updated_session.expires_at {
412                changes.push("expires_at".to_string());
413            }
414
415            // Update session
416            session_data.session = updated_session;
417            session_data.metadata.last_accessed_at = Utc::now();
418
419            if changes.contains(&"access_token".to_string())
420                || changes.contains(&"refresh_token".to_string())
421            {
422                session_data.metadata.last_refreshed_at = Some(Utc::now());
423            }
424
425            // Store in memory
426            {
427                let mut sessions = self.active_sessions.write();
428                sessions.insert(session_id, session_data.clone());
429            }
430
431            // Persist to storage
432            let key = format!("{}{}", self.config.session_key_prefix, session_id);
433            let expires_at = Some(session_data.session.expires_at);
434            self.config
435                .storage_backend
436                .store_session(&key, &session_data, expires_at)
437                .await?;
438
439            // Emit event
440            self.emit_session_event(SessionEvent::Updated {
441                session_id,
442                changes,
443            });
444
445            // Cross-tab sync
446            if self.config.enable_cross_tab_sync {
447                self.sync_to_other_tabs(session_id, "session_updated")
448                    .await?;
449            }
450        } else {
451            return Err(Error::auth(format!("Session {} not found", session_id)));
452        }
453
454        Ok(())
455    }
456
457    /// Remove a session
458    pub async fn remove_session(&self, session_id: Uuid, reason: String) -> Result<()> {
459        // Remove from memory
460        {
461            let mut sessions = self.active_sessions.write();
462            sessions.remove(&session_id);
463        }
464
465        // Remove from storage
466        let key = format!("{}{}", self.config.session_key_prefix, session_id);
467        self.config.storage_backend.remove_session(&key).await?;
468
469        // Emit event
470        self.emit_session_event(SessionEvent::Destroyed { session_id, reason });
471
472        // Cross-tab sync
473        if self.config.enable_cross_tab_sync {
474            self.sync_to_other_tabs(session_id, "session_destroyed")
475                .await?;
476        }
477
478        Ok(())
479    }
480
481    /// List all active sessions
482    pub async fn list_sessions(&self) -> Result<Vec<SessionData>> {
483        let sessions = self.active_sessions.read();
484        Ok(sessions.values().cloned().collect())
485    }
486
487    /// Add session event listener
488    pub fn on_session_event<F>(&self, callback: F) -> Uuid
489    where
490        F: Fn(SessionEvent) + Send + Sync + 'static,
491    {
492        let listener_id = Uuid::new_v4();
493        let mut listeners = self.event_listeners.write();
494        listeners.insert(listener_id, Box::new(callback));
495        listener_id
496    }
497
498    /// Remove session event listener
499    pub fn remove_event_listener(&self, listener_id: Uuid) {
500        let mut listeners = self.event_listeners.write();
501        listeners.remove(&listener_id);
502    }
503
504    /// Private helper methods
505    async fn load_persisted_sessions(&self) -> Result<()> {
506        let keys = self.config.storage_backend.list_session_keys().await?;
507        let mut valid_sessions = Vec::new();
508        let mut expired_keys = Vec::new();
509
510        // Collect valid sessions and expired keys without holding lock
511        for key in keys {
512            if let Some(session_data) = self.config.storage_backend.get_session(&key).await? {
513                if session_data.session.expires_at > Utc::now() {
514                    if let Ok(uuid) = key
515                        .strip_prefix(&self.config.session_key_prefix)
516                        .unwrap_or(&key)
517                        .parse::<Uuid>()
518                    {
519                        valid_sessions.push((uuid, session_data));
520                    }
521                } else {
522                    expired_keys.push(key);
523                }
524            }
525        }
526
527        // Insert valid sessions (acquire lock once)
528        {
529            let mut sessions = self.active_sessions.write();
530            for (uuid, session_data) in valid_sessions {
531                sessions.insert(uuid, session_data);
532            }
533        }
534
535        // Remove expired sessions
536        for key in expired_keys {
537            let _ = self.config.storage_backend.remove_session(&key).await;
538        }
539
540        Ok(())
541    }
542
543    async fn setup_cross_tab_sync(&self) -> Result<()> {
544        // Platform-specific cross-tab channel setup
545        #[cfg(target_arch = "wasm32")]
546        {
547            let channel = crate::session::wasm::WasmCrossTabChannel::new()?;
548            let mut cross_tab = self.cross_tab_channel.lock();
549            *cross_tab = Some(Box::new(channel));
550        }
551
552        #[cfg(not(target_arch = "wasm32"))]
553        {
554            let channel = crate::session::native::NativeCrossTabChannel::new()?;
555            let mut cross_tab = self.cross_tab_channel.lock();
556            *cross_tab = Some(Box::new(channel));
557        }
558
559        Ok(())
560    }
561
562    async fn start_background_tasks(&self) -> Result<()> {
563        // Start session cleanup task
564        // Start sync task
565        // Start monitoring task
566
567        // TODO: Implement background tasks with tokio or wasm timers
568
569        Ok(())
570    }
571
572    #[allow(clippy::await_holding_lock)]
573    async fn sync_to_other_tabs(&self, session_id: Uuid, event_type: &str) -> Result<()> {
574        if let Some(channel) = self.cross_tab_channel.lock().as_ref() {
575            let message = CrossTabMessage {
576                message_id: Uuid::new_v4(),
577                session_id,
578                event_type: event_type.to_string(),
579                payload: serde_json::json!({}),
580                timestamp: Utc::now(),
581                source_tab: self
582                    .detect_tab_id()
583                    .unwrap_or_else(|| "unknown".to_string()),
584            };
585
586            channel.send_message(message).await?;
587        }
588
589        Ok(())
590    }
591
592    fn emit_session_event(&self, event: SessionEvent) {
593        let listeners = self.event_listeners.read();
594        for callback in listeners.values() {
595            callback(event.clone());
596        }
597    }
598
599    fn detect_device_id(&self) -> Option<String> {
600        // TODO: Implement device ID detection
601        None
602    }
603
604    fn detect_client_id(&self) -> Option<String> {
605        // TODO: Implement client ID detection
606        None
607    }
608
609    fn detect_tab_id(&self) -> Option<String> {
610        // TODO: Implement tab ID detection
611        None
612    }
613
614    fn detect_session_source(&self) -> SessionSource {
615        #[cfg(target_arch = "wasm32")]
616        {
617            SessionSource::Web {
618                tab_id: self.detect_tab_id(),
619            }
620        }
621        #[cfg(not(target_arch = "wasm32"))]
622        {
623            SessionSource::Desktop { app_version: None }
624        }
625    }
626
627    fn detect_user_agent(&self) -> Option<String> {
628        #[cfg(target_arch = "wasm32")]
629        {
630            web_sys::window().and_then(|w| w.navigator().user_agent().ok())
631        }
632        #[cfg(not(target_arch = "wasm32"))]
633        {
634            None
635        }
636    }
637}
638
639#[cfg(feature = "session-management")]
640impl Default for SessionManagerConfig {
641    fn default() -> Self {
642        Self {
643            storage_backend: Arc::new(StorageBackend::Memory(
644                crate::session::storage::MemoryStorage::new(),
645            )),
646            enable_cross_tab_sync: true,
647            session_key_prefix: "supabase_session_".to_string(),
648            default_expiry_seconds: 3600, // 1 hour
649            enable_encryption: false,
650            encryption_key: None,
651            enable_monitoring: true,
652            max_memory_sessions: 100,
653            sync_interval_seconds: 30,
654        }
655    }
656}