refactor: decouple core-storage services from static keys and update i18n setup to use configurable storage adapters
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@repo/core-api": "workspace:*",
|
||||
"@repo/core-i18n": "workspace:*",
|
||||
"@repo/core-storage": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createLocalStorage } from '@repo/core-storage';
|
||||
|
||||
export const AppStorageKey = {
|
||||
LOCALE: 'app_locale',
|
||||
} as const;
|
||||
|
||||
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
||||
|
||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.LOCALE,
|
||||
]);
|
||||
|
||||
export const secureStorage = createLocalStorage<AppStorageKeyValue>({
|
||||
plainTextKeys: PLAIN_KEYS
|
||||
});
|
||||
@@ -16,10 +16,16 @@ import './main.css';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { setupI18n } from '@repo/core-i18n';
|
||||
import { secureStorage, AppStorageKey } from './core/storage';
|
||||
import App from './app';
|
||||
|
||||
async function bootstrap() {
|
||||
await setupI18n();
|
||||
await setupI18n({
|
||||
storageAdapter: {
|
||||
getLanguage: async () => await secureStorage.getItem<string>(AppStorageKey.LOCALE),
|
||||
setLanguage: async (lng: string) => await secureStorage.setItem(AppStorageKey.LOCALE, lng),
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById('app')!).render(
|
||||
<StrictMode>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAppEvent } from '@repo/core-events';
|
||||
import type { ProfileUpdatedPayload } from '@repo/core-events';
|
||||
import { secureIndexedDB } from '@repo/core-storage';
|
||||
import { secureIndexedDB, AppStorageKey } from '../../../../core/storage';
|
||||
|
||||
// ─── Props ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -14,45 +14,16 @@ interface StorageSyncListenerProps {
|
||||
/**
|
||||
* Headless component that listens to `AUTH:PROFILE_UPDATED` events
|
||||
* and persists the profile data to IndexedDB via `@repo/core-storage`.
|
||||
*
|
||||
* This component renders nothing — it is purely a side-effect listener.
|
||||
* Mount it anywhere in the React tree; it will auto-cleanup on unmount.
|
||||
*
|
||||
* **Architecture notes for production use:**
|
||||
*
|
||||
* 1. The `StorageKey` registry in `@repo/core-storage` should include
|
||||
* a `USER_PROFILE` key (which it already does — see `storage.key.ts`).
|
||||
* This means `secureIndexedDB.setItem('user_profile', payload)` will
|
||||
* automatically encrypt the data at rest because `user_profile` is
|
||||
* listed in `ENCRYPTED_KEYS`.
|
||||
*
|
||||
* 2. If you need to store additional event-driven data, extend `StorageKey`:
|
||||
* ```ts
|
||||
* // In packages/core-storage/src/storage.key.ts:
|
||||
* export const StorageKey = {
|
||||
* ...existing,
|
||||
* LAST_PROFILE_SYNC: 'last_profile_sync',
|
||||
* } as const;
|
||||
* ```
|
||||
*
|
||||
* 3. For bidirectional sync (storage → event), consider adding a
|
||||
* `STORAGE:PROFILE_LOADED` event to `AppEventRegistry` that fires when
|
||||
* the app reads the profile from IndexedDB on boot.
|
||||
*
|
||||
* 4. Error handling: In production, wrap the `setItem` call in a
|
||||
* retry mechanism or queue failed writes to a dead-letter store.
|
||||
*/
|
||||
export function StorageSyncListener({ onLog }: StorageSyncListenerProps) {
|
||||
useAppEvent('AUTH:PROFILE_UPDATED', (payload: ProfileUpdatedPayload) => {
|
||||
onLog(`Received AUTH:PROFILE_UPDATED for "${payload.name}" (${payload.email})`);
|
||||
|
||||
// Persist to IndexedDB via @repo/core-storage.
|
||||
// Uses StorageKey.USER_PROFILE ('user_profile') which is in ENCRYPTED_KEYS,
|
||||
// so the data will be AES-encrypted at rest automatically.
|
||||
// Persist to IndexedDB via local AppStorageKey.
|
||||
secureIndexedDB
|
||||
.setItem('user_profile', payload)
|
||||
.setItem(AppStorageKey.USER_PROFILE, payload)
|
||||
.then(() => {
|
||||
onLog(`✅ Profile persisted to IndexedDB (key: "user_profile", encrypted: true)`);
|
||||
onLog(`✅ Profile persisted to IndexedDB (key: "${AppStorageKey.USER_PROFILE}", encrypted: true)`);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
onLog(`❌ IndexedDB write failed: ${err.message}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation, changeLanguage, applyTenantOverrides, i18n } from '@repo/core-i18n';
|
||||
import { secureIndexedDB } from '@repo/core-storage';
|
||||
import { secureIndexedDB, AppStorageKey } from '../../../../../../core/storage';
|
||||
|
||||
// Decentralized locale imports
|
||||
import bookingId from '../locales/id/booking.json';
|
||||
@@ -41,7 +41,7 @@ export default function I18nSample() {
|
||||
const [adminHeaderTitle, setAdminHeaderTitle] = useState('Daftar Pengeluaran');
|
||||
const [dbPayloadStr, setDbPayloadStr] = useState<string>('No data in DB');
|
||||
|
||||
const MOCK_DB_KEY = 'mock_db_company_a';
|
||||
const MOCK_DB_KEY = AppStorageKey.MOCK_DB_COMPANY_A;
|
||||
|
||||
const loadDbPayload = useCallback(async () => {
|
||||
try {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { secureStorage, secureIndexedDB, StorageKey } from '@repo/core-storage';
|
||||
import { secureStorage, secureIndexedDB, AppStorageKey } from '../../../../../../core/storage';
|
||||
|
||||
// ─── Demo Data ──────────────────────────────────────────────────
|
||||
|
||||
interface DemoUser {
|
||||
id: number;
|
||||
user: string;
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,16 @@ interface DemoDraft {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const DEMO_USER: DemoUser = { id: 1, user: 'Firman', role: 'admin' };
|
||||
const DEMO_USER: DemoUser = {
|
||||
id: 'u-123',
|
||||
name: 'Firman Ramdhani',
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
const DEMO_DRAFT: DemoDraft = { id: 101, type: 'offline_draft', content: 'Draft data saved offline' };
|
||||
|
||||
const LS_KEY = StorageKey.USER_PROFILE; // Encrypted at rest (in ENCRYPTED_KEYS)
|
||||
const IDB_KEY = 'offline_draft'; // Plain key for IndexedDB demo
|
||||
const LS_KEY = AppStorageKey.USER_PROFILE; // Encrypted at rest (in ENCRYPTED_KEYS)
|
||||
const IDB_KEY = AppStorageKey.OFFLINE_DRAFT; // Plain key for IndexedDB demo
|
||||
|
||||
// ─── Shared Styles ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
|
||||
|
||||
export const AppStorageKey = {
|
||||
USER_PROFILE: 'user_profile',
|
||||
LOCALE: 'app_locale',
|
||||
ACCESS_TOKEN: 'access_token',
|
||||
REFRESH_TOKEN: 'refresh_token',
|
||||
MOCK_DB_COMPANY_A: 'mock_db_company_a',
|
||||
OFFLINE_DRAFT: 'offline_draft',
|
||||
} as const;
|
||||
|
||||
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
||||
|
||||
export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.USER_PROFILE,
|
||||
AppStorageKey.ACCESS_TOKEN,
|
||||
AppStorageKey.REFRESH_TOKEN,
|
||||
]);
|
||||
|
||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.LOCALE,
|
||||
AppStorageKey.MOCK_DB_COMPANY_A,
|
||||
AppStorageKey.OFFLINE_DRAFT,
|
||||
]);
|
||||
|
||||
export const secureStorage = createLocalStorage<AppStorageKeyValue>({
|
||||
encryptedKeys: ENCRYPTED_KEYS,
|
||||
plainTextKeys: PLAIN_KEYS,
|
||||
});
|
||||
export const secureIndexedDB = createIndexedDB<AppStorageKeyValue>({
|
||||
dbName: 'eigen_erp_db',
|
||||
storeName: 'web_store',
|
||||
encryptedKeys: ENCRYPTED_KEYS,
|
||||
plainTextKeys: PLAIN_KEYS,
|
||||
});
|
||||
@@ -16,12 +16,18 @@ import './main.css';
|
||||
import { lazy, StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { setupI18n } from '@repo/core-i18n';
|
||||
import { secureStorage, AppStorageKey } from './core/storage';
|
||||
|
||||
const App = lazy(() => import('./apps'));
|
||||
|
||||
async function bootstrap() {
|
||||
// Initialize i18next and load language from secureStorage
|
||||
await setupI18n();
|
||||
await setupI18n({
|
||||
storageAdapter: {
|
||||
getLanguage: async () => await secureStorage.getItem<string>(AppStorageKey.LOCALE),
|
||||
setLanguage: async (lng: string) => await secureStorage.setItem(AppStorageKey.LOCALE, lng),
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById('app')!).render(
|
||||
<StrictMode>
|
||||
|
||||
Reference in New Issue
Block a user