refactor: decouple core-storage services from static keys and update i18n setup to use configurable storage adapters

This commit is contained in:
Firman Ramdhani
2026-05-28 13:27:13 +07:00
parent b4af762baa
commit d0ebceb1bb
20 changed files with 377 additions and 375 deletions
@@ -1,54 +1,50 @@
import { EncryptionUtils } from '@repo/utils';
import type { IStorageService } from './storage.interface';
import { ENCRYPTED_KEYS } from './storage.key';
export interface StorageOptions<TKey extends string> {
encryptedKeys?: Set<TKey>;
plainTextKeys?: Set<TKey>;
}
/**
* Enterprise-grade localStorage wrapper with optional AES encryption.
*
* Keys listed in `ENCRYPTED_KEYS` are automatically encrypted before
* writing and decrypted on read using `@repo/utils` `EncryptionUtils`.
* All other keys are stored as plain JSON.
*
* All methods are async (returning Promises) to conform to the
* `IStorageService` interface, ensuring consumers can swap between
* localStorage and IndexedDB without code changes.
*
* @example
* ```ts
* const storage = new LocalStorageService();
*
* // Encrypted at rest (ACCESS_TOKEN is in ENCRYPTED_KEYS)
* await storage.setItem(StorageKey.ACCESS_TOKEN, 'eyJhbGci...');
*
* // Plain JSON (THEME is NOT in ENCRYPTED_KEYS)
* await storage.setItem(StorageKey.THEME, 'dark');
* ```
*/
export class LocalStorageService implements IStorageService {
export class LocalStorageService<TKey extends string> implements IStorageService<TKey> {
private readonly encryption: EncryptionUtils;
private readonly encryptedKeys: Set<TKey>;
private readonly plainTextKeys: Set<TKey>;
constructor(encryptionUtils?: EncryptionUtils) {
constructor(options?: StorageOptions<TKey>, encryptionUtils?: EncryptionUtils) {
this.encryption = encryptionUtils ?? EncryptionUtils.getInstance();
this.encryptedKeys = options?.encryptedKeys ?? new Set();
this.plainTextKeys = options?.plainTextKeys ?? new Set();
}
/** Check if a key should be encrypted. */
private shouldEncrypt(key: string): boolean {
return ENCRYPTED_KEYS.has(key);
private validateKey(key: TKey): void {
if (!this.encryptedKeys.has(key) && !this.plainTextKeys.has(key)) {
throw new Error(`[Storage Engine] Security Exception: Key '${key}' is not registered and cannot be accessed.`);
}
}
async setItem<T>(key: string, value: T): Promise<void> {
private shouldEncrypt(key: TKey): boolean {
return this.encryptedKeys.has(key);
}
async setItem<T>(key: TKey, value: T): Promise<void> {
this.validateKey(key);
const serialized = JSON.stringify(value);
if (this.shouldEncrypt(key)) {
const encrypted = this.encryption.encrypt(serialized);
localStorage.setItem(key, encrypted);
localStorage.setItem(key as string, encrypted);
} else {
localStorage.setItem(key, serialized);
localStorage.setItem(key as string, serialized);
}
}
async getItem<T>(key: string): Promise<T | null> {
const raw = localStorage.getItem(key);
async getItem<T>(key: TKey): Promise<T | null> {
this.validateKey(key);
const raw = localStorage.getItem(key as string);
if (raw === null) return null;
try {
@@ -60,29 +56,36 @@ export class LocalStorageService implements IStorageService {
return JSON.parse(raw) as T;
} catch {
console.warn(`[core-storage] Failed to parse key "${key}". Removing corrupt entry.`);
localStorage.removeItem(key);
localStorage.removeItem(key as string);
return null;
}
}
async removeItem(key: string): Promise<void> {
localStorage.removeItem(key);
async removeItem(key: TKey): Promise<void> {
this.validateKey(key);
localStorage.removeItem(key as string);
}
async clear(): Promise<void> {
localStorage.clear();
}
async hasItem(key: string): Promise<boolean> {
return localStorage.getItem(key) !== null;
async hasItem(key: TKey): Promise<boolean> {
return localStorage.getItem(key as string) !== null;
}
async keys(): Promise<string[]> {
const result: string[] = [];
async keys(): Promise<TKey[]> {
const result: TKey[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key !== null) result.push(key);
if (key !== null) result.push(key as TKey);
}
return result;
}
}
export function createLocalStorage<TKey extends string>(
options?: StorageOptions<TKey>
): IStorageService<TKey> {
return new LocalStorageService<TKey>(options);
}