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#[async_trait::async_trait]
49pub trait SessionStorage: Send + Sync {
50 async fn store_session(
52 &self,
53 key: &str,
54 session: &SessionData,
55 expires_at: Option<DateTime<Utc>>,
56 ) -> Result<()>;
57
58 async fn get_session(&self, key: &str) -> Result<Option<SessionData>>;
60
61 async fn remove_session(&self, key: &str) -> Result<()>;
63
64 async fn clear_all_sessions(&self) -> Result<()>;
66
67 async fn list_session_keys(&self) -> Result<Vec<String>>;
69
70 fn is_available(&self) -> bool;
72}
73
74#[cfg(feature = "session-management")]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SessionData {
78 pub session: Session,
80
81 pub metadata: SessionMetadata,
83
84 pub platform_data: HashMap<String, serde_json::Value>,
86}
87
88#[cfg(feature = "session-management")]
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SessionMetadata {
92 pub session_id: Uuid,
94
95 pub device_id: Option<String>,
97
98 pub client_id: Option<String>,
100
101 pub created_at: DateTime<Utc>,
103
104 pub last_accessed_at: DateTime<Utc>,
106
107 pub last_refreshed_at: Option<DateTime<Utc>>,
109
110 pub source: SessionSource,
112
113 pub ip_address: Option<String>,
115
116 pub user_agent: Option<String>,
118
119 pub location: Option<SessionLocation>,
121
122 pub tags: Vec<String>,
124
125 pub custom: HashMap<String, serde_json::Value>,
127}
128
129#[cfg(feature = "session-management")]
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub enum SessionSource {
133 Web { tab_id: Option<String> },
135 Mobile { app_version: Option<String> },
137 Desktop { app_version: Option<String> },
139 Server { service: Option<String> },
141 Cli { tool_name: Option<String> },
143 Other { description: String },
145}
146
147#[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)>, }
157
158#[cfg(feature = "session-management")]
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum SessionEvent {
162 Created { session_id: Uuid },
164 Updated {
166 session_id: Uuid,
167 changes: Vec<String>,
168 },
169 Accessed {
171 session_id: Uuid,
172 timestamp: DateTime<Utc>,
173 },
174 Refreshed {
176 session_id: Uuid,
177 timestamp: DateTime<Utc>,
178 },
179 Expired {
181 session_id: Uuid,
182 timestamp: DateTime<Utc>,
183 },
184 Destroyed { session_id: Uuid, reason: String },
186 CrossTabSync {
188 session_id: Uuid,
189 source_tab: String,
190 },
191 Conflict {
193 session_id: Uuid,
194 conflict_type: String,
195 },
196}
197
198#[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#[cfg(feature = "session-management")]
212#[derive(Debug, Clone)]
213pub struct SessionManagerConfig {
214 pub storage_backend: Arc<StorageBackend>,
216
217 pub enable_cross_tab_sync: bool,
219
220 pub session_key_prefix: String,
222
223 pub default_expiry_seconds: i64,
225
226 pub enable_encryption: bool,
228
229 pub encryption_key: Option<[u8; 32]>,
231
232 pub enable_monitoring: bool,
234
235 pub max_memory_sessions: usize,
237
238 pub sync_interval_seconds: u64,
240}
241
242#[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#[cfg(feature = "session-management")]
253pub type SessionEventCallback = Box<dyn Fn(SessionEvent) + Send + Sync + 'static>;
254
255#[cfg(feature = "session-management")]
257#[async_trait::async_trait]
258pub trait CrossTabChannel: Send + Sync {
259 async fn send_message(&self, message: CrossTabMessage) -> Result<()>;
261
262 fn on_message(&self, callback: Box<dyn Fn(CrossTabMessage) + Send + Sync>);
264
265 async fn close(&self) -> Result<()>;
267}
268
269#[cfg(feature = "session-management")]
270impl SessionManager {
271 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 pub async fn initialize(&self) -> Result<()> {
283 self.load_persisted_sessions().await?;
285
286 if self.config.enable_cross_tab_sync {
288 self.setup_cross_tab_sync().await?;
289 }
290
291 self.start_background_tasks().await?;
293
294 Ok(())
295 }
296
297 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, user_agent: self.detect_user_agent(),
312 location: None, 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 {
325 let mut sessions = self.active_sessions.write();
326 sessions.insert(session_id, session_data.clone());
327 }
328
329 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 self.emit_session_event(SessionEvent::Created { session_id });
339
340 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 pub async fn get_session(&self, session_id: Uuid) -> Result<Option<SessionData>> {
351 {
353 let sessions = self.active_sessions.read();
354 if let Some(session_data) = sessions.get(&session_id) {
355 let mut updated_data = session_data.clone();
357 updated_data.metadata.last_accessed_at = Utc::now();
358
359 drop(sessions);
361 let mut sessions = self.active_sessions.write();
362 sessions.insert(session_id, updated_data.clone());
363
364 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 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 session_data.metadata.last_accessed_at = Utc::now();
379
380 {
382 let mut sessions = self.active_sessions.write();
383 sessions.insert(session_id, session_data.clone());
384 }
385
386 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 pub async fn update_session(&self, session_id: Uuid, updated_session: Session) -> Result<()> {
400 let mut changes = Vec::new();
401
402 if let Some(mut session_data) = self.get_session(session_id).await? {
404 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 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 {
427 let mut sessions = self.active_sessions.write();
428 sessions.insert(session_id, session_data.clone());
429 }
430
431 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 self.emit_session_event(SessionEvent::Updated {
441 session_id,
442 changes,
443 });
444
445 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 pub async fn remove_session(&self, session_id: Uuid, reason: String) -> Result<()> {
459 {
461 let mut sessions = self.active_sessions.write();
462 sessions.remove(&session_id);
463 }
464
465 let key = format!("{}{}", self.config.session_key_prefix, session_id);
467 self.config.storage_backend.remove_session(&key).await?;
468
469 self.emit_session_event(SessionEvent::Destroyed { session_id, reason });
471
472 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 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 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 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 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 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 {
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 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 #[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 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 None
602 }
603
604 fn detect_client_id(&self) -> Option<String> {
605 None
607 }
608
609 fn detect_tab_id(&self) -> Option<String> {
610 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, enable_encryption: false,
650 encryption_key: None,
651 enable_monitoring: true,
652 max_memory_sessions: 100,
653 sync_interval_seconds: 30,
654 }
655 }
656}