/**
* 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 };