import localforage from 'localforage'; class CacheService { constructor() { this.storage = localforage.createInstance({ name: 'LinAppCache', storeName: 'authCache' }); this.cachePrefix = 'lin_cache_'; this.defaultTTL = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds } /** * Generate a cache key with prefix * @param {string} key - The cache key * @returns {string} - Full cache key */ _generateKey(key) { return `${this.cachePrefix}${key}`; } /** * Set data in cache with TTL * @param {string} key - Cache key * @param {any} data - Data to cache * @param {number} ttl - Time to live in milliseconds (optional) * @returns {Promise} */ async set(key, data, ttl = this.defaultTTL) { const cacheKey = this._generateKey(key); const expiry = Date.now() + ttl; const cacheData = { data, expiry, metadata: { createdAt: Date.now(), ttl, key } }; try { await this.storage.setItem(cacheKey, cacheData); if (import.meta.env.VITE_NODE_ENV === 'development') { console.log(`🗃️ [Cache] Set cache for key: ${key}`); } } catch (error) { console.error(`🗃️ [Cache] Error setting cache for key: ${key}`, error); throw error; } } /** * Get data from cache * @param {string} key - Cache key * @returns {Promise} - Cached data or null if not found/expired */ async get(key) { const cacheKey = this._generateKey(key); try { const cacheData = await this.storage.getItem(cacheKey); if (!cacheData) { if (import.meta.env.VITE_NODE_ENV === 'development') { console.log(`🗃️ [Cache] Cache miss for key: ${key}`); } return null; } // Check if cache has expired if (Date.now() > cacheData.expiry) { await this.remove(key); if (import.meta.env.VITE_NODE_ENV === 'development') { console.log(`🗃️ [Cache] Cache expired for key: ${key}`); } return null; } if (import.meta.env.VITE_NODE_ENV === 'development') { console.log(`🗃️ [Cache] Cache hit for key: ${key}`); } return cacheData.data; } catch (error) { console.error(`🗃️ [Cache] Error getting cache for key: ${key}`, error); return null; } } /** * Remove data from cache * @param {string} key - Cache key * @returns {Promise} */ async remove(key) { const cacheKey = this._generateKey(key); try { await this.storage.removeItem(cacheKey); if (import.meta.env.VITE_NODE_ENV === 'development') { console.log(`🗃️ [Cache] Removed cache for key: ${key}`); } } catch (error) { console.error(`🗃️ [Cache] Error removing cache for key: ${key}`, error); throw error; } } /** * Clear all cached data * @returns {Promise} */ async clear() { try { await this.storage.clear(); if (import.meta.env.VITE_NODE_ENV === 'development') { console.log('🗃️ [Cache] Cleared all cache'); } } catch (error) { console.error('🗃️ [Cache] Error clearing cache', error); throw error; } } /** * Check if cache exists and is valid * @param {string} key - Cache key * @returns {Promise} - True if cache exists and is valid */ async exists(key) { const cacheKey = this._generateKey(key); try { const cacheData = await this.storage.getItem(cacheKey); if (!cacheData) { return false; } // Check if cache has expired if (Date.now() > cacheData.expiry) { await this.remove(key); return false; } return true; } catch (error) { console.error(`🗃️ [Cache] Error checking cache existence for key: ${key}`, error); return false; } } /** * Get cache metadata * @param {string} key - Cache key * @returns {Promise} - Cache metadata or null */ async getMetadata(key) { const cacheKey = this._generateKey(key); try { const cacheData = await this.storage.getItem(cacheKey); if (!cacheData) { return null; } return cacheData.metadata || null; } catch (error) { console.error(`🗃️ [Cache] Error getting cache metadata for key: ${key}`, error); return null; } } /** * Set user authentication cache * @param {Object} authData - Authentication data * @param {boolean} rememberMe - Remember me flag * @returns {Promise} */ async setAuthCache(authData, rememberMe = false) { const ttl = rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session await this.set('auth_token', authData.token, ttl); await this.set('user_data', authData.user, ttl); await this.set('remember_me', rememberMe, ttl); await this.set('auth_expiry', Date.now() + ttl, ttl); // Store device fingerprint for security const deviceFingerprint = this.generateDeviceFingerprint(); await this.set('device_fingerprint', deviceFingerprint, ttl); } /** * Get user authentication cache * @returns {Promise} - Authentication cache data or null */ async getAuthCache() { try { const [token, userData, rememberMe, expiry, deviceFingerprint] = await Promise.all([ this.get('auth_token'), this.get('user_data'), this.get('remember_me'), this.get('auth_expiry'), this.get('device_fingerprint') ]); if (!token || !userData || !expiry) { return null; } // Check if cache has expired if (Date.now() > expiry) { await this.clearAuthCache(); return null; } // Validate device fingerprint for security const currentFingerprint = this.generateDeviceFingerprint(); if (deviceFingerprint && deviceFingerprint !== currentFingerprint) { await this.clearAuthCache(); return null; } return { token, user: userData, rememberMe: rememberMe || false, expiresAt: expiry, deviceFingerprint }; } catch (error) { console.error('🗃️ [Cache] Error getting auth cache', error); return null; } } /** * Clear authentication cache * @returns {Promise} */ async clearAuthCache() { const keys = ['auth_token', 'user_data', 'remember_me', 'auth_expiry', 'device_fingerprint']; await Promise.all(keys.map(key => this.remove(key))); // Also clear localStorage for consistency localStorage.removeItem('token'); localStorage.removeItem('rememberMePreference'); } /** * Generate device fingerprint for security * @returns {string} - Device fingerprint */ generateDeviceFingerprint() { const userAgent = navigator.userAgent; const screenResolution = `${screen.width}x${screen.height}`; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const language = navigator.language; // Create a simple hash-like string const fingerprint = `${userAgent}-${screenResolution}-${timezone}-${language}`; return btoa(fingerprint).replace(/[^a-zA-Z0-9]/g, '').substring(0, 32); } /** * Cache user preferences * @param {Object} preferences - User preferences * @returns {Promise} */ async setUserPreferences(preferences) { await this.set('user_preferences', preferences, 30 * 24 * 60 * 60 * 1000); // 30 days } /** * Get user preferences * @returns {Promise} - User preferences or null */ async getUserPreferences() { return await this.get('user_preferences'); } /** * Cache API responses to reduce network requests * @param {string} endpoint - API endpoint * @param {Object} data - Response data * @param {number} ttl - Cache TTL in milliseconds * @returns {Promise} */ async cacheApiResponse(endpoint, data, ttl = 5 * 60 * 1000) { // 5 minutes default await this.set(`api_${endpoint}`, data, ttl); } /** * Get cached API response * @param {string} endpoint - API endpoint * @returns {Promise} - Cached response or null */ async getCachedApiResponse(endpoint) { return await this.get(`api_${endpoint}`); } /** * Set authentication token * @param {string} token - JWT token * @param {boolean} rememberMe - Remember me flag * @returns {Promise} */ async setAuthToken(token, rememberMe = false) { const ttl = rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session await this.set('auth_token', token, ttl); await this.set('remember_me', rememberMe, ttl); await this.set('auth_expiry', Date.now() + ttl, ttl); } /** * Get authentication token * @returns {Promise} - Auth token data or null */ async getAuthToken() { try { const [token, rememberMe, expiry] = await Promise.all([ this.get('auth_token'), this.get('remember_me'), this.get('auth_expiry') ]); if (!token || !expiry) { return null; } // Check if cache has expired if (Date.now() > expiry) { await this.clearAuthToken(); return null; } return { token, rememberMe: rememberMe || false, expiresAt: expiry }; } catch (error) { console.error('🗃️ [Cache] Error getting auth token', error); return null; } } /** * Clear authentication token * @returns {Promise} */ async clearAuthToken() { const keys = ['auth_token', 'remember_me', 'auth_expiry']; await Promise.all(keys.map(key => this.remove(key))); localStorage.removeItem('token'); localStorage.removeItem('rememberMePreference'); } /** * Set user data * @param {Object} userData - User data object * @param {boolean} rememberMe - Remember me flag * @returns {Promise} */ async setUserData(userData, rememberMe = false) { const ttl = rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session await this.set('user_data', userData, ttl); } /** * Get user data * @returns {Promise} - User data or null */ async getUserData() { return await this.get('user_data'); } /** * Set session information * @param {Object} sessionData - Session data * @returns {Promise} */ async setSessionInfo(sessionData) { const ttl = sessionData.rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session await this.set('session_info', sessionData, ttl); } /** * Get session information * @returns {Promise} - Session data or null */ async getSessionInfo() { return await this.get('session_info'); } } // Export singleton instance export default new CacheService();