supabase/session/
storage.rs

1//! Session storage backends
2//!
3//! This module provides different storage backends for session persistence:
4//! - MemoryStorage: In-memory storage for testing and temporary sessions
5//! - LocalStorage: Browser localStorage backend for WASM
6//! - FileSystemStorage: Filesystem backend for native applications
7//! - EncryptedStorage: Wrapper for encrypted storage
8
9// Type alias for complex storage type
10#[cfg(feature = "session-management")]
11type SessionEntry = (SessionData, Option<DateTime<Utc>>);
12
13#[cfg(feature = "session-management")]
14use crate::error::{Error, Result};
15#[cfg(feature = "session-management")]
16use crate::session::{SessionData, SessionStorage};
17#[cfg(feature = "session-management")]
18use chrono::{DateTime, Utc};
19#[cfg(feature = "session-management")]
20use std::collections::HashMap;
21#[cfg(feature = "session-management")]
22use std::sync::{Arc, RwLock};
23
24/// In-memory session storage (not persistent across restarts)
25#[cfg(feature = "session-management")]
26#[derive(Debug)]
27pub struct MemoryStorage {
28    sessions: Arc<RwLock<HashMap<String, SessionEntry>>>,
29}
30
31#[cfg(feature = "session-management")]
32impl MemoryStorage {
33    pub fn new() -> Self {
34        Self {
35            sessions: Arc::new(RwLock::new(HashMap::new())),
36        }
37    }
38
39    /// Clean up expired sessions
40    pub fn cleanup_expired(&self) {
41        let now = Utc::now();
42        let mut sessions = self.sessions.write().unwrap();
43        sessions.retain(|_, (_, expires_at)| {
44            match expires_at {
45                Some(expiry) => *expiry > now,
46                None => true, // Keep sessions without expiry
47            }
48        });
49    }
50}
51
52#[cfg(feature = "session-management")]
53#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
54#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
55impl SessionStorage for MemoryStorage {
56    async fn store_session(
57        &self,
58        key: &str,
59        session: &SessionData,
60        expires_at: Option<DateTime<Utc>>,
61    ) -> Result<()> {
62        let mut sessions = self
63            .sessions
64            .write()
65            .map_err(|_| Error::storage("Failed to acquire write lock for memory storage"))?;
66        sessions.insert(key.to_string(), (session.clone(), expires_at));
67        Ok(())
68    }
69
70    async fn get_session(&self, key: &str) -> Result<Option<SessionData>> {
71        let sessions = self
72            .sessions
73            .read()
74            .map_err(|_| Error::storage("Failed to acquire read lock for memory storage"))?;
75
76        if let Some((session_data, expires_at)) = sessions.get(key) {
77            // Check if session is expired
78            if let Some(expiry) = expires_at {
79                if *expiry <= Utc::now() {
80                    return Ok(None);
81                }
82            }
83            Ok(Some(session_data.clone()))
84        } else {
85            Ok(None)
86        }
87    }
88
89    async fn remove_session(&self, key: &str) -> Result<()> {
90        let mut sessions = self
91            .sessions
92            .write()
93            .map_err(|_| Error::storage("Failed to acquire write lock for memory storage"))?;
94        sessions.remove(key);
95        Ok(())
96    }
97
98    async fn clear_all_sessions(&self) -> Result<()> {
99        let mut sessions = self
100            .sessions
101            .write()
102            .map_err(|_| Error::storage("Failed to acquire write lock for memory storage"))?;
103        sessions.clear();
104        Ok(())
105    }
106
107    async fn list_session_keys(&self) -> Result<Vec<String>> {
108        let sessions = self
109            .sessions
110            .read()
111            .map_err(|_| Error::storage("Failed to acquire read lock for memory storage"))?;
112        Ok(sessions.keys().cloned().collect())
113    }
114
115    fn is_available(&self) -> bool {
116        true
117    }
118}
119
120#[cfg(feature = "session-management")]
121impl Default for MemoryStorage {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127/// Browser localStorage backend for WASM
128#[cfg(all(
129    feature = "session-management",
130    target_arch = "wasm32",
131    feature = "wasm"
132))]
133#[derive(Debug)]
134pub struct LocalStorage {
135    key_prefix: String,
136}
137
138// WASM is single-threaded, so Send and Sync are safe to implement
139#[cfg(all(
140    feature = "session-management",
141    target_arch = "wasm32",
142    feature = "wasm"
143))]
144unsafe impl Send for LocalStorage {}
145#[cfg(all(
146    feature = "session-management",
147    target_arch = "wasm32",
148    feature = "wasm"
149))]
150unsafe impl Sync for LocalStorage {}
151
152#[cfg(all(
153    feature = "session-management",
154    target_arch = "wasm32",
155    feature = "wasm"
156))]
157impl LocalStorage {
158    pub fn new(key_prefix: Option<String>) -> Result<Self> {
159        // Check if localStorage is available
160        if !Self::is_storage_available() {
161            return Err(Error::storage(
162                "localStorage is not available in this environment",
163            ));
164        }
165
166        Ok(Self {
167            key_prefix: key_prefix.unwrap_or_else(|| "supabase_".to_string()),
168        })
169    }
170
171    fn is_storage_available() -> bool {
172        web_sys::window()
173            .and_then(|w| w.local_storage().ok().flatten())
174            .is_some()
175    }
176
177    fn get_storage() -> Result<web_sys::Storage> {
178        web_sys::window()
179            .ok_or_else(|| Error::storage("No window object available"))?
180            .local_storage()
181            .map_err(|_| Error::storage("Failed to access localStorage"))?
182            .ok_or_else(|| Error::storage("localStorage is not available"))
183    }
184
185    fn make_key(&self, key: &str) -> String {
186        format!("{}{}", self.key_prefix, key)
187    }
188}
189
190#[cfg(all(
191    feature = "session-management",
192    target_arch = "wasm32",
193    feature = "wasm"
194))]
195#[async_trait::async_trait(?Send)]
196impl SessionStorage for LocalStorage {
197    async fn store_session(
198        &self,
199        key: &str,
200        session: &SessionData,
201        _expires_at: Option<DateTime<Utc>>,
202    ) -> Result<()> {
203        let storage = Self::get_storage()?;
204        let storage_key = self.make_key(key);
205        let serialized = serde_json::to_string(session)
206            .map_err(|e| Error::storage(format!("Failed to serialize session: {}", e)))?;
207
208        storage
209            .set_item(&storage_key, &serialized)
210            .map_err(|_| Error::storage("Failed to store session in localStorage"))?;
211
212        Ok(())
213    }
214
215    async fn get_session(&self, key: &str) -> Result<Option<SessionData>> {
216        let storage = Self::get_storage()?;
217        let storage_key = self.make_key(key);
218
219        match storage.get_item(&storage_key) {
220            Ok(Some(serialized)) => {
221                let session_data: SessionData = serde_json::from_str(&serialized)
222                    .map_err(|e| Error::storage(format!("Failed to deserialize session: {}", e)))?;
223
224                // Check if session is expired
225                if session_data.session.expires_at <= Utc::now() {
226                    // Remove expired session
227                    let _ = self.remove_session(key).await;
228                    Ok(None)
229                } else {
230                    Ok(Some(session_data))
231                }
232            }
233            Ok(None) => Ok(None),
234            Err(_) => Err(Error::storage("Failed to read from localStorage")),
235        }
236    }
237
238    async fn remove_session(&self, key: &str) -> Result<()> {
239        let storage = Self::get_storage()?;
240        let storage_key = self.make_key(key);
241        storage
242            .remove_item(&storage_key)
243            .map_err(|_| Error::storage("Failed to remove session from localStorage"))?;
244        Ok(())
245    }
246
247    async fn clear_all_sessions(&self) -> Result<()> {
248        let storage = Self::get_storage()?;
249        let keys_to_remove: Vec<String> = (0..storage.length().unwrap_or(0))
250            .filter_map(|i| storage.key(i).ok().flatten())
251            .filter(|key| key.starts_with(&self.key_prefix))
252            .collect();
253
254        for key in keys_to_remove {
255            storage
256                .remove_item(&key)
257                .map_err(|_| Error::storage("Failed to clear session from localStorage"))?;
258        }
259
260        Ok(())
261    }
262
263    async fn list_session_keys(&self) -> Result<Vec<String>> {
264        let storage = Self::get_storage()?;
265        let keys: Vec<String> = (0..storage.length().unwrap_or(0))
266            .filter_map(|i| storage.key(i).ok().flatten())
267            .filter(|key| key.starts_with(&self.key_prefix))
268            .map(|key| {
269                key.strip_prefix(&self.key_prefix)
270                    .unwrap_or(&key)
271                    .to_string()
272            })
273            .collect();
274
275        Ok(keys)
276    }
277
278    fn is_available(&self) -> bool {
279        Self::is_storage_available()
280    }
281}
282
283/// Filesystem-based storage for native applications
284#[cfg(all(feature = "session-management", not(target_arch = "wasm32")))]
285#[derive(Debug)]
286pub struct FileSystemStorage {
287    base_dir: std::path::PathBuf,
288}
289
290#[cfg(all(feature = "session-management", not(target_arch = "wasm32")))]
291impl FileSystemStorage {
292    pub fn new(base_dir: Option<std::path::PathBuf>) -> Result<Self> {
293        let base_dir = match base_dir {
294            Some(dir) => dir,
295            None => {
296                // Use OS-appropriate data directory
297                dirs::data_local_dir()
298                    .ok_or_else(|| Error::storage("Could not determine data directory"))?
299                    .join("supabase-sessions")
300            }
301        };
302
303        // Create directory if it doesn't exist
304        std::fs::create_dir_all(&base_dir)
305            .map_err(|e| Error::storage(format!("Failed to create session directory: {}", e)))?;
306
307        Ok(Self { base_dir })
308    }
309
310    fn get_session_path(&self, key: &str) -> std::path::PathBuf {
311        self.base_dir.join(format!("{}.json", key))
312    }
313}
314
315#[cfg(all(feature = "session-management", not(target_arch = "wasm32")))]
316#[async_trait::async_trait]
317impl SessionStorage for FileSystemStorage {
318    async fn store_session(
319        &self,
320        key: &str,
321        session: &SessionData,
322        _expires_at: Option<DateTime<Utc>>,
323    ) -> Result<()> {
324        let path = self.get_session_path(key);
325        let serialized = serde_json::to_string_pretty(session)
326            .map_err(|e| Error::storage(format!("Failed to serialize session: {}", e)))?;
327
328        tokio::fs::write(&path, serialized)
329            .await
330            .map_err(|e| Error::storage(format!("Failed to write session file: {}", e)))?;
331
332        Ok(())
333    }
334
335    async fn get_session(&self, key: &str) -> Result<Option<SessionData>> {
336        let path = self.get_session_path(key);
337
338        match tokio::fs::read_to_string(&path).await {
339            Ok(serialized) => {
340                let session_data: SessionData = serde_json::from_str(&serialized)
341                    .map_err(|e| Error::storage(format!("Failed to deserialize session: {}", e)))?;
342
343                // Check if session is expired
344                if session_data.session.expires_at <= Utc::now() {
345                    // Remove expired session
346                    let _ = self.remove_session(key).await;
347                    Ok(None)
348                } else {
349                    Ok(Some(session_data))
350                }
351            }
352            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
353            Err(e) => Err(Error::storage(format!(
354                "Failed to read session file: {}",
355                e
356            ))),
357        }
358    }
359
360    async fn remove_session(&self, key: &str) -> Result<()> {
361        let path = self.get_session_path(key);
362        match tokio::fs::remove_file(&path).await {
363            Ok(_) => Ok(()),
364            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), // Already removed
365            Err(e) => Err(Error::storage(format!(
366                "Failed to remove session file: {}",
367                e
368            ))),
369        }
370    }
371
372    async fn clear_all_sessions(&self) -> Result<()> {
373        let mut dir_entries = tokio::fs::read_dir(&self.base_dir)
374            .await
375            .map_err(|e| Error::storage(format!("Failed to read session directory: {}", e)))?;
376
377        while let Some(entry) = dir_entries
378            .next_entry()
379            .await
380            .map_err(|e| Error::storage(format!("Failed to read directory entry: {}", e)))?
381        {
382            let path = entry.path();
383            if path.extension().and_then(|s| s.to_str()) == Some("json") {
384                match tokio::fs::remove_file(&path).await {
385                    Ok(_) => {}
386                    Err(e) => {
387                        tracing::warn!("Failed to remove session file {:?}: {}", path, e);
388                    }
389                }
390            }
391        }
392
393        Ok(())
394    }
395
396    async fn list_session_keys(&self) -> Result<Vec<String>> {
397        let mut dir_entries = tokio::fs::read_dir(&self.base_dir)
398            .await
399            .map_err(|e| Error::storage(format!("Failed to read session directory: {}", e)))?;
400
401        let mut keys = Vec::new();
402
403        while let Some(entry) = dir_entries
404            .next_entry()
405            .await
406            .map_err(|e| Error::storage(format!("Failed to read directory entry: {}", e)))?
407        {
408            let path = entry.path();
409            if path.extension().and_then(|s| s.to_str()) == Some("json") {
410                if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) {
411                    keys.push(file_stem.to_string());
412                }
413            }
414        }
415
416        Ok(keys)
417    }
418
419    fn is_available(&self) -> bool {
420        self.base_dir.exists() && self.base_dir.is_dir()
421    }
422}
423
424/// Encrypted storage wrapper
425#[cfg(all(feature = "session-management", feature = "session-encryption"))]
426pub struct EncryptedStorage {
427    inner: Arc<dyn SessionStorage>,
428    encryptor: Arc<crate::session::encryption::SessionEncryptor>,
429}
430
431#[cfg(all(feature = "session-management", feature = "session-encryption"))]
432impl std::fmt::Debug for EncryptedStorage {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        f.debug_struct("EncryptedStorage")
435            .field("inner", &"Arc<dyn SessionStorage>")
436            .field("encryptor", &"Arc<SessionEncryptor>")
437            .finish()
438    }
439}
440
441#[cfg(all(feature = "session-management", feature = "session-encryption"))]
442impl EncryptedStorage {
443    pub fn new(inner: Arc<dyn SessionStorage>, encryption_key: [u8; 32]) -> Result<Self> {
444        let encryptor = Arc::new(crate::session::encryption::SessionEncryptor::new(
445            encryption_key,
446        )?);
447        Ok(Self { inner, encryptor })
448    }
449}
450
451#[cfg(all(feature = "session-management", feature = "session-encryption"))]
452#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
453#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
454impl SessionStorage for EncryptedStorage {
455    async fn store_session(
456        &self,
457        key: &str,
458        session: &SessionData,
459        expires_at: Option<DateTime<Utc>>,
460    ) -> Result<()> {
461        let encrypted_session = self.encryptor.encrypt_session(session)?;
462        self.inner
463            .store_session(key, &encrypted_session, expires_at)
464            .await
465    }
466
467    async fn get_session(&self, key: &str) -> Result<Option<SessionData>> {
468        if let Some(encrypted_session) = self.inner.get_session(key).await? {
469            let decrypted_session = self.encryptor.decrypt_session(&encrypted_session)?;
470            Ok(Some(decrypted_session))
471        } else {
472            Ok(None)
473        }
474    }
475
476    async fn remove_session(&self, key: &str) -> Result<()> {
477        self.inner.remove_session(key).await
478    }
479
480    async fn clear_all_sessions(&self) -> Result<()> {
481        self.inner.clear_all_sessions().await
482    }
483
484    async fn list_session_keys(&self) -> Result<Vec<String>> {
485        self.inner.list_session_keys().await
486    }
487
488    fn is_available(&self) -> bool {
489        self.inner.is_available()
490    }
491}
492
493/// Enum-based storage backend for dyn compatibility
494#[cfg(feature = "session-management")]
495#[derive(Debug)]
496pub enum StorageBackend {
497    Memory(MemoryStorage),
498    #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
499    LocalStorage(LocalStorage),
500    #[cfg(not(target_arch = "wasm32"))]
501    FileSystem(FileSystemStorage),
502    #[cfg(feature = "session-encryption")]
503    Encrypted(EncryptedStorage),
504}
505
506#[cfg(feature = "session-management")]
507impl StorageBackend {
508    /// Store a session with optional expiry
509    pub async fn store_session(
510        &self,
511        key: &str,
512        session: &SessionData,
513        expires_at: Option<DateTime<Utc>>,
514    ) -> Result<()> {
515        match self {
516            StorageBackend::Memory(storage) => {
517                storage.store_session(key, session, expires_at).await
518            }
519            #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
520            StorageBackend::LocalStorage(storage) => {
521                storage.store_session(key, session, expires_at).await
522            }
523            #[cfg(not(target_arch = "wasm32"))]
524            StorageBackend::FileSystem(storage) => {
525                storage.store_session(key, session, expires_at).await
526            }
527            #[cfg(feature = "session-encryption")]
528            StorageBackend::Encrypted(storage) => {
529                storage.store_session(key, session, expires_at).await
530            }
531        }
532    }
533
534    /// Retrieve a session by key
535    pub async fn get_session(&self, key: &str) -> Result<Option<SessionData>> {
536        match self {
537            StorageBackend::Memory(storage) => storage.get_session(key).await,
538            #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
539            StorageBackend::LocalStorage(storage) => storage.get_session(key).await,
540            #[cfg(not(target_arch = "wasm32"))]
541            StorageBackend::FileSystem(storage) => storage.get_session(key).await,
542            #[cfg(feature = "session-encryption")]
543            StorageBackend::Encrypted(storage) => storage.get_session(key).await,
544        }
545    }
546
547    /// Remove a session by key
548    pub async fn remove_session(&self, key: &str) -> Result<()> {
549        match self {
550            StorageBackend::Memory(storage) => storage.remove_session(key).await,
551            #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
552            StorageBackend::LocalStorage(storage) => storage.remove_session(key).await,
553            #[cfg(not(target_arch = "wasm32"))]
554            StorageBackend::FileSystem(storage) => storage.remove_session(key).await,
555            #[cfg(feature = "session-encryption")]
556            StorageBackend::Encrypted(storage) => storage.remove_session(key).await,
557        }
558    }
559
560    /// Clear all sessions
561    pub async fn clear_all_sessions(&self) -> Result<()> {
562        match self {
563            StorageBackend::Memory(storage) => storage.clear_all_sessions().await,
564            #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
565            StorageBackend::LocalStorage(storage) => storage.clear_all_sessions().await,
566            #[cfg(not(target_arch = "wasm32"))]
567            StorageBackend::FileSystem(storage) => storage.clear_all_sessions().await,
568            #[cfg(feature = "session-encryption")]
569            StorageBackend::Encrypted(storage) => storage.clear_all_sessions().await,
570        }
571    }
572
573    /// List all session keys
574    pub async fn list_session_keys(&self) -> Result<Vec<String>> {
575        match self {
576            StorageBackend::Memory(storage) => storage.list_session_keys().await,
577            #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
578            StorageBackend::LocalStorage(storage) => storage.list_session_keys().await,
579            #[cfg(not(target_arch = "wasm32"))]
580            StorageBackend::FileSystem(storage) => storage.list_session_keys().await,
581            #[cfg(feature = "session-encryption")]
582            StorageBackend::Encrypted(storage) => storage.list_session_keys().await,
583        }
584    }
585
586    /// Check if storage is available
587    pub fn is_available(&self) -> bool {
588        match self {
589            StorageBackend::Memory(storage) => storage.is_available(),
590            #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
591            StorageBackend::LocalStorage(storage) => storage.is_available(),
592            #[cfg(not(target_arch = "wasm32"))]
593            StorageBackend::FileSystem(storage) => storage.is_available(),
594            #[cfg(feature = "session-encryption")]
595            StorageBackend::Encrypted(storage) => storage.is_available(),
596        }
597    }
598}
599
600/// Factory function to create the appropriate storage backend
601#[cfg(feature = "session-management")]
602pub fn create_default_storage() -> Result<Arc<StorageBackend>> {
603    #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
604    {
605        if let Ok(storage) = LocalStorage::new(None) {
606            Ok(Arc::new(StorageBackend::LocalStorage(storage)))
607        } else {
608            // Fallback to memory storage
609            Ok(Arc::new(StorageBackend::Memory(MemoryStorage::new())))
610        }
611    }
612
613    #[cfg(all(target_arch = "wasm32", not(feature = "wasm")))]
614    {
615        // Without wasm feature, only memory storage is available
616        Ok(Arc::new(StorageBackend::Memory(MemoryStorage::new())))
617    }
618
619    #[cfg(not(target_arch = "wasm32"))]
620    {
621        if let Ok(storage) = FileSystemStorage::new(None) {
622            Ok(Arc::new(StorageBackend::FileSystem(storage)))
623        } else {
624            // Fallback to memory storage
625            Ok(Arc::new(StorageBackend::Memory(MemoryStorage::new())))
626        }
627    }
628}