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