UNPKG

@qikdev/sdk

Version:

Promise based Javascript SDK

307 lines (257 loc) 8.03 kB
/** * Storage abstraction layer for handling both localStorage and httpOnly cookie storage * @module QikStorage */ /** * Base storage interface */ class StorageAdapter { constructor() { if (this.constructor === StorageAdapter) { throw new Error("StorageAdapter is an abstract class and cannot be instantiated directly"); } } /** * Get user session data * @returns {Object|null} User session object */ getUser() { throw new Error("getUser method must be implemented"); } /** * Set user session data * @param {Object} user User session object */ setUser(user) { throw new Error("setUser method must be implemented"); } /** * Clear user session data */ clearUser() { throw new Error("clearUser method must be implemented"); } /** * Get current access token * @returns {String|null} Access token */ getAccessToken() { throw new Error("getAccessToken method must be implemented"); } /** * Get current refresh token * @returns {String|null} Refresh token */ getRefreshToken() { throw new Error("getRefreshToken method must be implemented"); } /** * Get token expiry date * @returns {String|null} Token expiry date */ getTokenExpiry() { throw new Error("getTokenExpiry method must be implemented"); } /** * Check if storage is available * @returns {Boolean} Whether storage is available */ isAvailable() { throw new Error("isAvailable method must be implemented"); } } /** * localStorage-based storage adapter (current behavior) */ class LocalStorageAdapter extends StorageAdapter { constructor() { super(); this.storage = {}; } getUser() { return this.storage.user || null; } setUser(user) { this.storage.user = user; } clearUser() { delete this.storage.user; } getAccessToken() { const user = this.getUser(); return user?.token?.accessToken || null; } getRefreshToken() { const user = this.getUser(); return user?.token?.refreshToken || null; } getTokenExpiry() { const user = this.getUser(); return user?.token?.expires || null; } isAvailable() { return true; // Always available as it uses in-memory storage } } /** * Cookie-based storage adapter for httpOnly cookies */ class CookieStorageAdapter extends StorageAdapter { constructor(options = {}) { super(); this.options = { domain: options.domain || this._detectDomain(), secure: options.secure !== false, // Default to true sameSite: options.sameSite || 'lax', ...options }; this.sessionStorage = {}; // In-memory storage for session data } /** * Auto-detect appropriate cookie domain * @private */ _detectDomain() { if (typeof window === 'undefined') return null; const hostname = window.location.hostname; // For localhost, don't set domain if (hostname === 'localhost' || hostname === '127.0.0.1') { return null; } // For IP addresses, don't set domain if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { return null; } // For subdomains, use parent domain (e.g., .example.com) const parts = hostname.split('.'); if (parts.length > 2) { return '.' + parts.slice(-2).join('.'); } return null; // Let browser handle single domain } /** * Read a cookie value * @private */ _getCookie(name) { if (typeof document === 'undefined') return null; const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { return parts.pop().split(';').shift(); } return null; } /** * Set a cookie value (for non-httpOnly cookies only) * @private */ _setCookie(name, value, options = {}) { if (typeof document === 'undefined') return; const opts = { ...this.options, ...options }; let cookieString = `${name}=${value}`; if (opts.domain) { cookieString += `; Domain=${opts.domain}`; } if (opts.secure) { cookieString += '; Secure'; } if (opts.sameSite) { cookieString += `; SameSite=${opts.sameSite}`; } if (opts.maxAge) { cookieString += `; Max-Age=${opts.maxAge}`; } document.cookie = cookieString; } /** * Delete a cookie * @private */ _deleteCookie(name) { this._setCookie(name, '', { maxAge: 0 }); } getUser() { return this.sessionStorage.user || null; } setUser(user) { if (user) { // Store session data in memory (accessible to JavaScript) const { token, ...sessionData } = user; this.sessionStorage.user = sessionData; // Tokens are handled by httpOnly cookies set by the server // We don't store them in JavaScript for security } else { this.sessionStorage.user = null; } } clearUser() { delete this.sessionStorage.user; // Note: httpOnly cookies will be cleared by server response // But we can clear any non-httpOnly cookies we might have set this._deleteCookie('qik_session_id'); } getAccessToken() { // In cookie mode, we can't access httpOnly cookies from JavaScript // The browser will automatically include them in requests // Return null to indicate tokens are handled by cookies return null; } getRefreshToken() { // Same as access token - handled by httpOnly cookies return null; } getTokenExpiry() { // We can't read httpOnly cookie expiry from JavaScript // The server will handle token expiry validation return null; } isAvailable() { // Check if we're in a browser environment and cookies are enabled if (typeof document === 'undefined') return false; try { // Test if we can set/read cookies const testCookie = 'qik_test_cookie'; this._setCookie(testCookie, 'test'); const canRead = this._getCookie(testCookie) === 'test'; this._deleteCookie(testCookie); return canRead; } catch (e) { return false; } } /** * Check if we're in cookie mode * @returns {Boolean} */ isCookieMode() { return true; } /** * Get cookie configuration options * @returns {Object} */ getCookieOptions() { return { ...this.options }; } } /** * Factory function to create appropriate storage adapter * @param {Object} options Configuration options * @param {Boolean} options.useHttpOnlyCookies Whether to use cookie storage * @param {Object} options.cookieConfig Cookie configuration options * @returns {StorageAdapter} Storage adapter instance */ export function createStorageAdapter(options = {}) { if (options.useHttpOnlyCookies) { const cookieAdapter = new CookieStorageAdapter(options.cookieConfig); // Fallback to localStorage if cookies aren't available if (!cookieAdapter.isAvailable()) { console.warn('Qik SDK: Cookies not available, falling back to localStorage mode'); return new LocalStorageAdapter(); } return cookieAdapter; } return new LocalStorageAdapter(); } export { StorageAdapter, LocalStorageAdapter, CookieStorageAdapter };