1pub 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#[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#[cfg(feature = "session-management")]
44use storage::StorageBackend;
45
46#[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 async fn store_session(
53 &self,
54 key: &str,
55 session: &SessionData,
56 expires_at: Option<DateTime<Utc>>,
57 ) -> Result<()>;
58
59 async fn get_session(&self, key: &str) -> Result<Option<SessionData>>;
61
62 async fn remove_session(&self, key: &str) -> Result<()>;
64
65 async fn clear_all_sessions(&self) -> Result<()>;
67
68 async fn list_session_keys(&self) -> Result<Vec<String>>;
70
71 fn is_available(&self) -> bool;
73}
74
75#[cfg(feature = "session-management")]
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct SessionData {
79 pub session: Session,
81
82 pub metadata: SessionMetadata,
84
85 pub platform_data: HashMap<String, serde_json::Value>,
87}
88
89#[cfg(feature = "session-management")]
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SessionMetadata {
93 pub session_id: Uuid,
95
96 pub device_id: Option<String>,
98
99 pub client_id: Option<String>,
101
102 pub created_at: DateTime<Utc>,
104
105 pub last_accessed_at: DateTime<Utc>,
107
108 pub last_refreshed_at: Option<DateTime<Utc>>,
110
111 pub source: SessionSource,
113
114 pub ip_address: Option<String>,
116
117 pub user_agent: Option<String>,
119
120 pub location: Option<SessionLocation>,
122
123 pub tags: Vec<String>,
125
126 pub custom: HashMap<String, serde_json::Value>,
128}
129
130#[cfg(feature = "session-management")]
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub enum SessionSource {
134 Web { tab_id: Option<String> },
136 Mobile { app_version: Option<String> },
138 Desktop { app_version: Option<String> },
140 Server { service: Option<String> },
142 Cli { tool_name: Option<String> },
144 Other { description: String },
146}
147
148#[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)>, }
158
159#[cfg(feature = "session-management")]
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub enum SessionEvent {
163 Created { session_id: Uuid },
165 Updated {
167 session_id: Uuid,
168 changes: Vec<String>,
169 },
170 Accessed {
172 session_id: Uuid,
173 timestamp: DateTime<Utc>,
174 },
175 Refreshed {
177 session_id: Uuid,
178 timestamp: DateTime<Utc>,
179 },
180 Expired {
182 session_id: Uuid,
183 timestamp: DateTime<Utc>,
184 },
185 Destroyed { session_id: Uuid, reason: String },
187 CrossTabSync {
189 session_id: Uuid,
190 source_tab: String,
191 },
192 Conflict {
194 session_id: Uuid,
195 conflict_type: String,
196 },
197}
198
199#[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#[cfg(feature = "session-management")]
213#[derive(Debug, Clone)]
214pub struct SessionManagerConfig {
215 pub storage_backend: Arc<StorageBackend>,
217
218 pub enable_cross_tab_sync: bool,
220
221 pub session_key_prefix: String,
223
224 pub default_expiry_seconds: i64,
226
227 pub enable_encryption: bool,
229
230 pub encryption_key: Option<[u8; 32]>,
232
233 pub enable_monitoring: bool,
235
236 pub max_memory_sessions: usize,
238
239 pub sync_interval_seconds: u64,
241}
242
243#[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#[cfg(feature = "session-management")]
254pub type SessionEventCallback = Box<dyn Fn(SessionEvent) + Send + Sync + 'static>;
255
256#[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 async fn send_message(&self, message: CrossTabMessage) -> Result<()>;
263
264 fn on_message(&self, callback: Box<dyn Fn(CrossTabMessage) + Send + Sync>);
266
267 async fn close(&self) -> Result<()>;
269}
270
271#[cfg(feature = "session-management")]
272impl SessionManager {
273 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 pub async fn initialize(&self) -> Result<()> {
285 self.load_persisted_sessions().await?;
287
288 if self.config.enable_cross_tab_sync {
290 self.setup_cross_tab_sync().await?;
291 }
292
293 self.start_background_tasks().await?;
295
296 Ok(())
297 }
298
299 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, user_agent: self.detect_user_agent(),
314 location: None, 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 {
327 let mut sessions = self.active_sessions.write();
328 sessions.insert(session_id, session_data.clone());
329 }
330
331 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 self.emit_session_event(SessionEvent::Created { session_id });
341
342 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 pub async fn get_session(&self, session_id: Uuid) -> Result<Option<SessionData>> {
353 {
355 let sessions = self.active_sessions.read();
356 if let Some(session_data) = sessions.get(&session_id) {
357 let mut updated_data = session_data.clone();
359 updated_data.metadata.last_accessed_at = Utc::now();
360
361 drop(sessions);
363 let mut sessions = self.active_sessions.write();
364 sessions.insert(session_id, updated_data.clone());
365
366 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 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 session_data.metadata.last_accessed_at = Utc::now();
381
382 {
384 let mut sessions = self.active_sessions.write();
385 sessions.insert(session_id, session_data.clone());
386 }
387
388 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 pub async fn update_session(&self, session_id: Uuid, updated_session: Session) -> Result<()> {
402 let mut changes = Vec::new();
403
404 if let Some(mut session_data) = self.get_session(session_id).await? {
406 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 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 {
429 let mut sessions = self.active_sessions.write();
430 sessions.insert(session_id, session_data.clone());
431 }
432
433 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 self.emit_session_event(SessionEvent::Updated {
443 session_id,
444 changes,
445 });
446
447 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 pub async fn remove_session(&self, session_id: Uuid, reason: String) -> Result<()> {
461 {
463 let mut sessions = self.active_sessions.write();
464 sessions.remove(&session_id);
465 }
466
467 let key = format!("{}{}", self.config.session_key_prefix, session_id);
469 self.config.storage_backend.remove_session(&key).await?;
470
471 self.emit_session_event(SessionEvent::Destroyed { session_id, reason });
473
474 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 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 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 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 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 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 {
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 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 #[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 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 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 None
610 }
611
612 fn detect_client_id(&self) -> Option<String> {
613 None
615 }
616
617 fn detect_tab_id(&self) -> Option<String> {
618 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, enable_encryption: false,
658 encryption_key: None,
659 enable_monitoring: true,
660 max_memory_sessions: 100,
661 sync_interval_seconds: 30,
662 }
663 }
664}