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