Start integrating the onboarding flow with backend 5
This commit is contained in:
@@ -1,397 +0,0 @@
|
||||
/**
|
||||
* Storage Service - Provides secure and consistent local/session storage management
|
||||
* with encryption, expiration, and type safety
|
||||
*/
|
||||
|
||||
interface StorageOptions {
|
||||
encrypt?: boolean;
|
||||
expiresIn?: number; // milliseconds
|
||||
storage?: 'local' | 'session';
|
||||
}
|
||||
|
||||
interface StorageItem<T = any> {
|
||||
value: T;
|
||||
encrypted?: boolean;
|
||||
expiresAt?: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
class StorageService {
|
||||
private readonly encryptionKey = 'bakery-app-key'; // In production, use proper key management
|
||||
|
||||
/**
|
||||
* Store data in browser storage
|
||||
*/
|
||||
setItem<T>(key: string, value: T, options: StorageOptions = {}): boolean {
|
||||
try {
|
||||
const {
|
||||
encrypt = false,
|
||||
expiresIn,
|
||||
storage = 'local'
|
||||
} = options;
|
||||
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
|
||||
const item: StorageItem = {
|
||||
value: encrypt ? this.encrypt(JSON.stringify(value)) : value,
|
||||
encrypted: encrypt,
|
||||
createdAt: Date.now(),
|
||||
...(expiresIn && { expiresAt: Date.now() + expiresIn })
|
||||
};
|
||||
|
||||
storageInstance.setItem(key, JSON.stringify(item));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Storage error setting item "${key}":`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data from browser storage
|
||||
*/
|
||||
getItem<T>(key: string, storage: 'local' | 'session' = 'local'): T | null {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const itemStr = storageInstance.getItem(key);
|
||||
|
||||
if (!itemStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item: StorageItem<T> = JSON.parse(itemStr);
|
||||
|
||||
// Check expiration
|
||||
if (item.expiresAt && Date.now() > item.expiresAt) {
|
||||
this.removeItem(key, storage);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle encrypted data
|
||||
if (item.encrypted && typeof item.value === 'string') {
|
||||
try {
|
||||
const decrypted = this.decrypt(item.value);
|
||||
return JSON.parse(decrypted);
|
||||
} catch (error) {
|
||||
console.error(`Failed to decrypt item "${key}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return item.value;
|
||||
} catch (error) {
|
||||
console.error(`Storage error getting item "${key}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from storage
|
||||
*/
|
||||
removeItem(key: string, storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
storageInstance.removeItem(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Storage error removing item "${key}":`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if item exists and is not expired
|
||||
*/
|
||||
hasItem(key: string, storage: 'local' | 'session' = 'local'): boolean {
|
||||
return this.getItem(key, storage) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all items from storage
|
||||
*/
|
||||
clear(storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
storageInstance.clear();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Storage error clearing storage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys from storage with optional prefix filter
|
||||
*/
|
||||
getKeys(prefix?: string, storage: 'local' | 'session' = 'local'): string[] {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const keys: string[] = [];
|
||||
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key && (!prefix || key.startsWith(prefix))) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
} catch (error) {
|
||||
console.error('Storage error getting keys:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage usage information
|
||||
*/
|
||||
getStorageInfo(storage: 'local' | 'session' = 'local'): {
|
||||
used: number;
|
||||
total: number;
|
||||
available: number;
|
||||
itemCount: number;
|
||||
} {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
|
||||
// Calculate used space (approximate)
|
||||
let used = 0;
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key) {
|
||||
const value = storageInstance.getItem(key);
|
||||
used += key.length + (value?.length || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Most browsers have ~5-10MB limit for localStorage
|
||||
const estimated_total = 5 * 1024 * 1024; // 5MB in bytes
|
||||
|
||||
return {
|
||||
used,
|
||||
total: estimated_total,
|
||||
available: estimated_total - used,
|
||||
itemCount: storageInstance.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Storage error getting storage info:', error);
|
||||
return { used: 0, total: 0, available: 0, itemCount: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean expired items from storage
|
||||
*/
|
||||
cleanExpired(storage: 'local' | 'session' = 'local'): number {
|
||||
let cleanedCount = 0;
|
||||
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key) {
|
||||
try {
|
||||
const itemStr = storageInstance.getItem(key);
|
||||
if (itemStr) {
|
||||
const item: StorageItem = JSON.parse(itemStr);
|
||||
if (item.expiresAt && Date.now() > item.expiresAt) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't parse the item, it might be corrupted
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
storageInstance.removeItem(key);
|
||||
cleanedCount++;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Storage error cleaning expired items:', error);
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup storage to JSON
|
||||
*/
|
||||
backup(storage: 'local' | 'session' = 'local'): string {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const backup: Record<string, any> = {};
|
||||
|
||||
for (let i = 0; i < storageInstance.length; i++) {
|
||||
const key = storageInstance.key(i);
|
||||
if (key) {
|
||||
const value = storageInstance.getItem(key);
|
||||
if (value) {
|
||||
backup[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
storage: storage,
|
||||
data: backup
|
||||
}, null, 2);
|
||||
} catch (error) {
|
||||
console.error('Storage error creating backup:', error);
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore storage from JSON backup
|
||||
*/
|
||||
restore(backupData: string, storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const backup = JSON.parse(backupData);
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
|
||||
if (backup.data) {
|
||||
Object.entries(backup.data).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
storageInstance.setItem(key, value);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Storage error restoring backup:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Encryption utilities (basic implementation - use proper crypto in production)
|
||||
private encrypt(text: string): string {
|
||||
try {
|
||||
// This is a simple XOR cipher - replace with proper encryption in production
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += String.fromCharCode(
|
||||
text.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length)
|
||||
);
|
||||
}
|
||||
return btoa(result);
|
||||
} catch (error) {
|
||||
console.error('Encryption error:', error);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
private decrypt(encryptedText: string): string {
|
||||
try {
|
||||
const text = atob(encryptedText);
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += String.fromCharCode(
|
||||
text.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Decryption error:', error);
|
||||
return encryptedText;
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for common operations
|
||||
/**
|
||||
* Store user authentication data
|
||||
*/
|
||||
setAuthData(data: {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
user_data?: any;
|
||||
tenant_id?: string;
|
||||
}): boolean {
|
||||
const success = [
|
||||
this.setItem('access_token', data.access_token, { encrypt: true }),
|
||||
data.refresh_token ? this.setItem('refresh_token', data.refresh_token, { encrypt: true }) : true,
|
||||
data.user_data ? this.setItem('user_data', data.user_data) : true,
|
||||
data.tenant_id ? this.setItem('tenant_id', data.tenant_id) : true,
|
||||
].every(Boolean);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all authentication data
|
||||
*/
|
||||
clearAuthData(): boolean {
|
||||
return [
|
||||
this.removeItem('access_token'),
|
||||
this.removeItem('refresh_token'),
|
||||
this.removeItem('user_data'),
|
||||
this.removeItem('tenant_id'),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store app preferences
|
||||
*/
|
||||
setPreferences(preferences: Record<string, any>): boolean {
|
||||
return this.setItem('app_preferences', preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app preferences
|
||||
*/
|
||||
getPreferences<T = Record<string, any>>(): T | null {
|
||||
return this.getItem<T>('app_preferences');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store temporary session data with automatic expiration
|
||||
*/
|
||||
setSessionData(key: string, data: any, expiresInMinutes: number = 30): boolean {
|
||||
return this.setItem(key, data, {
|
||||
storage: 'session',
|
||||
expiresIn: expiresInMinutes * 60 * 1000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get temporary session data
|
||||
*/
|
||||
getSessionData<T>(key: string): T | null {
|
||||
return this.getItem<T>(key, 'session');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check storage availability
|
||||
*/
|
||||
isStorageAvailable(storage: 'local' | 'session' = 'local'): boolean {
|
||||
try {
|
||||
const storageInstance = storage === 'session' ? sessionStorage : localStorage;
|
||||
const test = '__storage_test__';
|
||||
storageInstance.setItem(test, test);
|
||||
storageInstance.removeItem(test);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const storageService = new StorageService();
|
||||
|
||||
// Export class for testing or multiple instances
|
||||
export { StorageService };
|
||||
|
||||
// Legacy compatibility functions
|
||||
export const getStorageItem = <T>(key: string): T | null => storageService.getItem<T>(key);
|
||||
export const setStorageItem = <T>(key: string, value: T, options?: StorageOptions): boolean =>
|
||||
storageService.setItem(key, value, options);
|
||||
export const removeStorageItem = (key: string): boolean => storageService.removeItem(key);
|
||||
Reference in New Issue
Block a user