feat: replace demo storage instances with secure storage implementations across i18n and storage features

This commit is contained in:
Firman Ramdhani
2026-05-23 07:46:40 +07:00
parent b6ba41e120
commit 3621f837f6
6 changed files with 54 additions and 61 deletions
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { useTranslation, changeLanguage, applyTenantOverrides, i18n } from '@repo/core-i18n'; import { useTranslation, changeLanguage, applyTenantOverrides, i18n } from '@repo/core-i18n';
import { demoIndexedDB } from '@repo/core-storage'; import { secureIndexedDB } from '@repo/core-storage';
// Decentralized locale imports // Decentralized locale imports
import bookingId from '../locales/id/booking.json'; import bookingId from '../locales/id/booking.json';
@@ -45,7 +45,7 @@ export default function I18nSample() {
const loadDbPayload = useCallback(async () => { const loadDbPayload = useCallback(async () => {
try { try {
const data = await demoIndexedDB.getItem<any>(MOCK_DB_KEY); const data = await secureIndexedDB.getItem<any>(MOCK_DB_KEY);
setDbPayloadStr(data ? JSON.stringify(data, null, 2) : 'No data in DB'); setDbPayloadStr(data ? JSON.stringify(data, null, 2) : 'No data in DB');
setAdminHeaderTitle(data?.overrides?.header?.title || 'Daftar Pengeluaran'); setAdminHeaderTitle(data?.overrides?.header?.title || 'Daftar Pengeluaran');
setAdminModuleName(data?.overrides?.module_name || 'PENGELUARAN'); setAdminModuleName(data?.overrides?.module_name || 'PENGELUARAN');
@@ -66,7 +66,7 @@ export default function I18nSample() {
header: { title: adminHeaderTitle }, header: { title: adminHeaderTitle },
}, },
}; };
await demoIndexedDB.setItem(MOCK_DB_KEY, payload); await secureIndexedDB.setItem(MOCK_DB_KEY, payload);
setSyncStatus('✅ Saved tenant config to IndexedDB!'); setSyncStatus('✅ Saved tenant config to IndexedDB!');
await loadDbPayload(); await loadDbPayload();
}; };
@@ -74,7 +74,7 @@ export default function I18nSample() {
// ─── Mock API ─────────────────────────────────────────────────── // ─── Mock API ───────────────────────────────────────────────────
const mockFetchTenantConfig = async (companyId: string): Promise<any> => { const mockFetchTenantConfig = async (companyId: string): Promise<any> => {
if (companyId === 'company-a') { if (companyId === 'company-a') {
const data = await demoIndexedDB.getItem<any>(MOCK_DB_KEY); const data = await secureIndexedDB.getItem<any>(MOCK_DB_KEY);
if (!data) { if (!data) {
throw new Error('Company A config not found in DB. Please save via Admin Panel first.'); throw new Error('Company A config not found in DB. Please save via Admin Panel first.');
} }
@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { demoSecureStorage, demoIndexedDB, StorageKey } from '@repo/core-storage'; import { secureStorage, secureIndexedDB, StorageKey } from '@repo/core-storage';
// ─── Demo Data ────────────────────────────────────────────────── // ─── Demo Data ──────────────────────────────────────────────────
@@ -102,12 +102,12 @@ export default function StorageSample() {
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
const lsCreate = useCallback(async () => { const lsCreate = useCallback(async () => {
await demoSecureStorage.setItem(LS_KEY, DEMO_USER); await secureStorage.setItem(LS_KEY, DEMO_USER);
pushLog(`[LS] CREATE → Stored encrypted: ${JSON.stringify(DEMO_USER)}`); pushLog(`[LS] CREATE → Stored encrypted: ${JSON.stringify(DEMO_USER)}`);
}, [pushLog]); }, [pushLog]);
const lsRead = useCallback(async () => { const lsRead = useCallback(async () => {
const result = await demoSecureStorage.getItem<DemoUser>(LS_KEY); const result = await secureStorage.getItem<DemoUser>(LS_KEY);
if (result) { if (result) {
setLsResult(JSON.stringify(result, null, 2)); setLsResult(JSON.stringify(result, null, 2));
pushLog(`[LS] READ → Decrypted: ${JSON.stringify(result)}`); pushLog(`[LS] READ → Decrypted: ${JSON.stringify(result)}`);
@@ -118,24 +118,24 @@ export default function StorageSample() {
}, [pushLog]); }, [pushLog]);
const lsUpdate = useCallback(async () => { const lsUpdate = useCallback(async () => {
const existing = await demoSecureStorage.getItem<DemoUser>(LS_KEY); const existing = await secureStorage.getItem<DemoUser>(LS_KEY);
if (!existing) { if (!existing) {
pushLog('[LS] UPDATE → Failed: key does not exist. Create first.'); pushLog('[LS] UPDATE → Failed: key does not exist. Create first.');
return; return;
} }
const updated: DemoUser = { ...existing, role: 'superadmin', id: existing.id + 1 }; const updated: DemoUser = { ...existing, role: 'superadmin', id: existing.id + 1 };
await demoSecureStorage.setItem(LS_KEY, updated); await secureStorage.setItem(LS_KEY, updated);
pushLog(`[LS] UPDATE → Re-encrypted: ${JSON.stringify(updated)}`); pushLog(`[LS] UPDATE → Re-encrypted: ${JSON.stringify(updated)}`);
}, [pushLog]); }, [pushLog]);
const lsDelete = useCallback(async () => { const lsDelete = useCallback(async () => {
await demoSecureStorage.removeItem(LS_KEY); await secureStorage.removeItem(LS_KEY);
setLsResult('(deleted)'); setLsResult('(deleted)');
pushLog(`[LS] DELETE → Removed key "${LS_KEY}"`); pushLog(`[LS] DELETE → Removed key "${LS_KEY}"`);
}, [pushLog]); }, [pushLog]);
const lsClear = useCallback(async () => { const lsClear = useCallback(async () => {
await demoSecureStorage.clear(); await secureStorage.clear();
setLsResult('(cleared)'); setLsResult('(cleared)');
pushLog('[LS] CLEAR → All localStorage keys removed'); pushLog('[LS] CLEAR → All localStorage keys removed');
}, [pushLog]); }, [pushLog]);
@@ -146,7 +146,7 @@ export default function StorageSample() {
const idbCreate = useCallback(async () => { const idbCreate = useCallback(async () => {
try { try {
await demoIndexedDB.setItem(IDB_KEY, DEMO_DRAFT); await secureIndexedDB.setItem(IDB_KEY, DEMO_DRAFT);
pushLog(`[IDB] CREATE → Stored: ${JSON.stringify(DEMO_DRAFT)}`); pushLog(`[IDB] CREATE → Stored: ${JSON.stringify(DEMO_DRAFT)}`);
} catch (err) { } catch (err) {
pushLog(`[IDB] CREATE → ERROR: ${err instanceof Error ? err.message : String(err)}`); pushLog(`[IDB] CREATE → ERROR: ${err instanceof Error ? err.message : String(err)}`);
@@ -155,7 +155,7 @@ export default function StorageSample() {
const idbRead = useCallback(async () => { const idbRead = useCallback(async () => {
try { try {
const result = await demoIndexedDB.getItem<DemoDraft>(IDB_KEY); const result = await secureIndexedDB.getItem<DemoDraft>(IDB_KEY);
if (result) { if (result) {
setIdbResult(JSON.stringify(result, null, 2)); setIdbResult(JSON.stringify(result, null, 2));
pushLog(`[IDB] READ → Retrieved: ${JSON.stringify(result)}`); pushLog(`[IDB] READ → Retrieved: ${JSON.stringify(result)}`);
@@ -170,7 +170,7 @@ export default function StorageSample() {
const idbUpdate = useCallback(async () => { const idbUpdate = useCallback(async () => {
try { try {
const existing = await demoIndexedDB.getItem<DemoDraft>(IDB_KEY); const existing = await secureIndexedDB.getItem<DemoDraft>(IDB_KEY);
if (!existing) { if (!existing) {
pushLog('[IDB] UPDATE → Failed: key does not exist. Create first.'); pushLog('[IDB] UPDATE → Failed: key does not exist. Create first.');
return; return;
@@ -180,7 +180,7 @@ export default function StorageSample() {
id: existing.id + 1, id: existing.id + 1,
content: `Updated at ${new Date().toLocaleTimeString()}`, content: `Updated at ${new Date().toLocaleTimeString()}`,
}; };
await demoIndexedDB.setItem(IDB_KEY, updated); await secureIndexedDB.setItem(IDB_KEY, updated);
pushLog(`[IDB] UPDATE → Persisted: ${JSON.stringify(updated)}`); pushLog(`[IDB] UPDATE → Persisted: ${JSON.stringify(updated)}`);
} catch (err) { } catch (err) {
pushLog(`[IDB] UPDATE → ERROR: ${err instanceof Error ? err.message : String(err)}`); pushLog(`[IDB] UPDATE → ERROR: ${err instanceof Error ? err.message : String(err)}`);
@@ -189,7 +189,7 @@ export default function StorageSample() {
const idbDelete = useCallback(async () => { const idbDelete = useCallback(async () => {
try { try {
await demoIndexedDB.removeItem(IDB_KEY); await secureIndexedDB.removeItem(IDB_KEY);
setIdbResult('(deleted)'); setIdbResult('(deleted)');
pushLog(`[IDB] DELETE → Removed key "${IDB_KEY}"`); pushLog(`[IDB] DELETE → Removed key "${IDB_KEY}"`);
} catch (err) { } catch (err) {
@@ -199,7 +199,7 @@ export default function StorageSample() {
const idbClear = useCallback(async () => { const idbClear = useCallback(async () => {
try { try {
await demoIndexedDB.clear(); await secureIndexedDB.clear();
setIdbResult('(cleared)'); setIdbResult('(cleared)');
pushLog('[IDB] CLEAR → All IndexedDB entries removed'); pushLog('[IDB] CLEAR → All IndexedDB entries removed');
} catch (err) { } catch (err) {
+5 -9
View File
@@ -1,5 +1,5 @@
import i18n from 'i18next'; import i18n from 'i18next';
import { demoSecureStorage, StorageKey } from '@repo/core-storage'; import { secureStorage, StorageKey } from '@repo/core-storage';
/** /**
* Changes the active language, saves the preference locally, and optionally syncs with the backend. * Changes the active language, saves the preference locally, and optionally syncs with the backend.
@@ -9,14 +9,14 @@ import { demoSecureStorage, StorageKey } from '@repo/core-storage';
*/ */
export async function changeLanguage( export async function changeLanguage(
newLng: string, newLng: string,
syncCallback?: (newLng: string, prevLng: string) => Promise<void> syncCallback?: (newLng: string, prevLng: string) => Promise<void>,
): Promise<void> { ): Promise<void> {
const prevLng = i18n.language; const prevLng = i18n.language;
if (prevLng === newLng) return; if (prevLng === newLng) return;
// 1. Update local storage & i18next optimistically // 1. Update local storage & i18next optimistically
await demoSecureStorage.setItem(StorageKey.LOCALE, newLng); await secureStorage.setItem(StorageKey.LOCALE, newLng);
await i18n.changeLanguage(newLng); await i18n.changeLanguage(newLng);
// 2. Trigger optional backend sync // 2. Trigger optional backend sync
@@ -26,7 +26,7 @@ export async function changeLanguage(
} catch (error) { } catch (error) {
console.error('[i18n] Backend sync failed, rolling back language', error); console.error('[i18n] Backend sync failed, rolling back language', error);
// Rollback on failure // Rollback on failure
await demoSecureStorage.setItem(StorageKey.LOCALE, prevLng); await secureStorage.setItem(StorageKey.LOCALE, prevLng);
await i18n.changeLanguage(prevLng); await i18n.changeLanguage(prevLng);
throw error; // Rethrow so the caller can show an error toast throw error; // Rethrow so the caller can show an error toast
} }
@@ -43,11 +43,7 @@ export async function changeLanguage(
* @param overrides A deeply nested object containing the overridden string keys and values. * @param overrides A deeply nested object containing the overridden string keys and values.
* @param lng Specific language to override. Defaults to currently active language. * @param lng Specific language to override. Defaults to currently active language.
*/ */
export function applyTenantOverrides( export function applyTenantOverrides(namespace: string, overrides: Record<string, unknown>, lng?: string): void {
namespace: string,
overrides: Record<string, unknown>,
lng?: string
): void {
const targetLng = lng || i18n.language; const targetLng = lng || i18n.language;
// deep: true -> merges with existing keys rather than replacing the whole namespace // deep: true -> merges with existing keys rather than replacing the whole namespace
+11 -13
View File
@@ -1,6 +1,6 @@
import i18n from 'i18next'; import i18n from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import { demoSecureStorage, StorageKey } from '@repo/core-storage'; import { secureStorage, StorageKey } from '@repo/core-storage';
import commonEn from './locales/en/common.json'; import commonEn from './locales/en/common.json';
import commonId from './locales/id/common.json'; import commonId from './locales/id/common.json';
@@ -23,7 +23,7 @@ export const resources = {
export async function setupI18n(): Promise<void> { export async function setupI18n(): Promise<void> {
let initialLng = DEFAULT_LANGUAGE; let initialLng = DEFAULT_LANGUAGE;
try { try {
const storedLng = await demoSecureStorage.getItem<string>(StorageKey.LOCALE); const storedLng = await secureStorage.getItem<string>(StorageKey.LOCALE);
if (storedLng && SUPPORTED_LANGUAGES.includes(storedLng as SupportedLanguage)) { if (storedLng && SUPPORTED_LANGUAGES.includes(storedLng as SupportedLanguage)) {
initialLng = storedLng; initialLng = storedLng;
} }
@@ -31,17 +31,15 @@ export async function setupI18n(): Promise<void> {
console.warn('[i18n] Failed to read locale from storage', err); console.warn('[i18n] Failed to read locale from storage', err);
} }
await i18n await i18n.use(initReactI18next).init({
.use(initReactI18next) resources,
.init({ lng: initialLng,
resources, fallbackLng: DEFAULT_LANGUAGE,
lng: initialLng, defaultNS: 'common',
fallbackLng: DEFAULT_LANGUAGE, interpolation: {
defaultNS: 'common', escapeValue: false, // React already escapes values
interpolation: { },
escapeValue: false, // React already escapes values });
},
});
// Apply initial language to the DOM for SEO/Accessibility // Apply initial language to the DOM for SEO/Accessibility
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
+9 -9
View File
@@ -32,36 +32,36 @@ This package provides a unified, Promise-based interface for interacting with br
### 1. Secure Local Storage (Tokens, Profile) ### 1. Secure Local Storage (Tokens, Profile)
Use `demoSecureStorage` (or your app's configured instance) for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest. Use `secureStorage` (or your app's configured instance) for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest.
```typescript ```typescript
import { demoSecureStorage, StorageKey } from '@repo/core-storage'; import { secureStorage, StorageKey } from '@repo/core-storage';
import type { UserProfile } from '@/types'; import type { UserProfile } from '@/types';
// CREATE / UPDATE // CREATE / UPDATE
// If StorageKey.USER_PROFILE is in ENCRYPTED_KEYS, this is AES-encrypted automatically. // If StorageKey.USER_PROFILE is in ENCRYPTED_KEYS, this is AES-encrypted automatically.
await demoSecureStorage.setItem(StorageKey.USER_PROFILE, { await secureStorage.setItem(StorageKey.USER_PROFILE, {
id: 1, id: 1,
name: 'Firman', name: 'Firman',
role: 'admin' role: 'admin'
}); });
// READ // READ
const profile = await demoSecureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE); const profile = await secureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE);
if (profile) { if (profile) {
console.log('Welcome back,', profile.name); console.log('Welcome back,', profile.name);
} }
// DELETE // DELETE
await demoSecureStorage.removeItem(StorageKey.USER_PROFILE); await secureStorage.removeItem(StorageKey.USER_PROFILE);
``` ```
### 2. IndexedDB (Offline Data, Large Payloads) ### 2. IndexedDB (Offline Data, Large Payloads)
Use the pre-configured `demoIndexedDB` (or instantiate your own) for large, asynchronous data. It uses the exact same API and encryption pipeline. Use the pre-configured `secureIndexedDB` (or instantiate your own) for large, asynchronous data. It uses the exact same API and encryption pipeline.
```typescript ```typescript
import { demoIndexedDB } from '@repo/core-storage'; import { secureIndexedDB } from '@repo/core-storage';
interface DraftData { interface DraftData {
id: string; id: string;
@@ -70,14 +70,14 @@ interface DraftData {
} }
// Save a large draft offline // Save a large draft offline
await demoIndexedDB.setItem('offline_draft_123', { await secureIndexedDB.setItem('offline_draft_123', {
id: '123', id: '123',
content: 'Huge text content...', content: 'Huge text content...',
lastModified: Date.now() lastModified: Date.now()
}); });
// Retrieve the draft // Retrieve the draft
const draft = await demoIndexedDB.getItem<DraftData>('offline_draft_123'); const draft = await secureIndexedDB.getItem<DraftData>('offline_draft_123');
``` ```
--- ---
+9 -10
View File
@@ -22,28 +22,27 @@ import { IndexedDBService } from './indexed-db.service';
* *
* @example * @example
* ```ts * ```ts
* import { demoSecureStorage, StorageKey } from '@repo/core-storage'; * import { secureStorage, StorageKey } from '@repo/core-storage';
* *
* await demoSecureStorage.setItem(StorageKey.ACCESS_TOKEN, 'eyJhbGci...'); * await secureStorage.setItem(StorageKey.ACCESS_TOKEN, 'eyJhbGci...');
* const token = await demoSecureStorage.getItem<string>(StorageKey.ACCESS_TOKEN); * const token = await secureStorage.getItem<string>(StorageKey.ACCESS_TOKEN);
* ``` * ```
*/ */
export const demoSecureStorage = new LocalStorageService(); export const secureStorage = new LocalStorageService();
/** /**
* Default IndexedDB instance. * Default IndexedDB instance.
* *
* Uses `app_db` database with a `kv_store` object store. * Uses `app_db` database with a `kv_store` object store.
* Sensitive keys are encrypted at rest using the same * Sensitive keys are encrypted at rest using the same
* `EncryptionUtils` pipeline as `demoSecureStorage`. * `EncryptionUtils` pipeline as `secureStorage`.
* *
* @example * @example
* ```ts * ```ts
* import { demoIndexedDB } from '@repo/core-storage'; * import { secureIndexedDB } from '@repo/core-storage';
* *
* await demoIndexedDB.setItem('offline_draft', { content: '...' }); * await secureIndexedDB.setItem('offline_draft', { content: '...' });
* const draft = await demoIndexedDB.getItem<Draft>('offline_draft'); * const draft = await secureIndexedDB.getItem<Draft>('offline_draft');
* ``` * ```
*/ */
export const demoIndexedDB = new IndexedDBService({ dbName: 'app_db', storeName: 'kv_store' }); export const secureIndexedDB = new IndexedDBService({ dbName: 'app_db', storeName: 'kv_store' });
export const demoIndexedDB2 = new IndexedDBService({ dbName: 'app_db', storeName: 'kv_store_2' });