feat: implement core-storage package with unified promise-based interface and encrypted-at-rest support

This commit is contained in:
Firman Ramdhani
2026-05-22 22:23:02 +07:00
parent 9b265b8f46
commit 255704f867
15 changed files with 1091 additions and 4 deletions
+1
View File
@@ -14,6 +14,7 @@
},
"dependencies": {
"@repo/core-api": "workspace:*",
"@repo/core-storage": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
@@ -1,4 +1,5 @@
import BookingSample from "./features/booking/presentation/BookingSample";
import StorageSample from "./features/storage/presentation/StorageSample";
export default function ExamplePage() {
return <div className="bg-amber-200">example
@@ -6,5 +7,9 @@ export default function ExamplePage() {
<h1 className="text-2xl font-bold mb-4">Enterprise Web App</h1>
<BookingSample />
</div>
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Enterprise Web App</h1>
<StorageSample />
</div>
</div>;
}
@@ -0,0 +1,275 @@
import { useState, useCallback } from 'react';
import { demoSecureStorage, demoIndexedDB, StorageKey } from '@repo/core-storage';
// ─── Demo Data ──────────────────────────────────────────────────
interface DemoUser {
id: number;
user: string;
role: string;
}
interface DemoDraft {
id: number;
type: string;
content: string;
}
const DEMO_USER: DemoUser = { id: 1, user: 'Firman', 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
// ─── Shared Styles ──────────────────────────────────────────────
const btnStyle = (color: string) => ({
padding: '8px 16px',
fontSize: 14,
fontWeight: 600 as const,
cursor: 'pointer' as const,
background: color,
color: '#fff',
border: 'none',
borderRadius: 6,
});
const preStyle = {
marginTop: 16,
background: '#1e1e2e',
color: '#a6e3a1',
padding: 16,
borderRadius: 8,
minHeight: 60,
overflow: 'auto' as const,
fontSize: 13,
};
const logContainerStyle = {
background: '#0f0f17',
color: '#94a3b8',
padding: 12,
borderRadius: 8,
maxHeight: 200,
overflow: 'auto' as const,
fontSize: 12,
};
// ─── Reusable CRUD Button Row ───────────────────────────────────
interface CRUDAction {
label: string;
handler: () => void;
color: string;
}
function CRUDButtons({ actions }: { actions: CRUDAction[] }) {
return (
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{actions.map(({ label, handler, color }) => (
<button key={label} onClick={handler} style={btnStyle(color)}>
{label}
</button>
))}
</div>
);
}
// ─── Component ──────────────────────────────────────────────────
/**
* Interactive demo for `@repo/core-storage`.
*
* Demonstrates the full CRUD lifecycle for BOTH storage backends:
* - **localStorage** (encrypted via AES for sensitive keys)
* - **IndexedDB** (Promise-wrapped, suitable for large payloads)
*
* Open the browser's DevTools:
* - **Application β†’ Local Storage** to see AES-encrypted payloads
* - **Application β†’ IndexedDB β†’ app_db β†’ kv_store** to see IDB entries
*/
export default function StorageSample() {
const [lsResult, setLsResult] = useState<string>('(no data read yet)');
const [idbResult, setIdbResult] = useState<string>('(no data read yet)');
const [log, setLog] = useState<string[]>([]);
const pushLog = useCallback((msg: string) => {
setLog((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]);
}, []);
// ═══════════════════════════════════════════════════════════════
// ── localStorage CRUD ─────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════
const lsCreate = useCallback(async () => {
await demoSecureStorage.setItem(LS_KEY, DEMO_USER);
pushLog(`[LS] CREATE β†’ Stored encrypted: ${JSON.stringify(DEMO_USER)}`);
}, [pushLog]);
const lsRead = useCallback(async () => {
const result = await demoSecureStorage.getItem<DemoUser>(LS_KEY);
if (result) {
setLsResult(JSON.stringify(result, null, 2));
pushLog(`[LS] READ β†’ Decrypted: ${JSON.stringify(result)}`);
} else {
setLsResult('(null β€” no data found)');
pushLog('[LS] READ β†’ null (key does not exist)');
}
}, [pushLog]);
const lsUpdate = useCallback(async () => {
const existing = await demoSecureStorage.getItem<DemoUser>(LS_KEY);
if (!existing) {
pushLog('[LS] UPDATE β†’ Failed: key does not exist. Create first.');
return;
}
const updated: DemoUser = { ...existing, role: 'superadmin', id: existing.id + 1 };
await demoSecureStorage.setItem(LS_KEY, updated);
pushLog(`[LS] UPDATE β†’ Re-encrypted: ${JSON.stringify(updated)}`);
}, [pushLog]);
const lsDelete = useCallback(async () => {
await demoSecureStorage.removeItem(LS_KEY);
setLsResult('(deleted)');
pushLog(`[LS] DELETE β†’ Removed key "${LS_KEY}"`);
}, [pushLog]);
const lsClear = useCallback(async () => {
await demoSecureStorage.clear();
setLsResult('(cleared)');
pushLog('[LS] CLEAR β†’ All localStorage keys removed');
}, [pushLog]);
// ═══════════════════════════════════════════════════════════════
// ── IndexedDB CRUD ────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════
const idbCreate = useCallback(async () => {
try {
await demoIndexedDB.setItem(IDB_KEY, DEMO_DRAFT);
pushLog(`[IDB] CREATE β†’ Stored: ${JSON.stringify(DEMO_DRAFT)}`);
} catch (err) {
pushLog(`[IDB] CREATE β†’ ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
}, [pushLog]);
const idbRead = useCallback(async () => {
try {
const result = await demoIndexedDB.getItem<DemoDraft>(IDB_KEY);
if (result) {
setIdbResult(JSON.stringify(result, null, 2));
pushLog(`[IDB] READ β†’ Retrieved: ${JSON.stringify(result)}`);
} else {
setIdbResult('(null β€” no data found)');
pushLog('[IDB] READ β†’ null (key does not exist)');
}
} catch (err) {
pushLog(`[IDB] READ β†’ ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
}, [pushLog]);
const idbUpdate = useCallback(async () => {
try {
const existing = await demoIndexedDB.getItem<DemoDraft>(IDB_KEY);
if (!existing) {
pushLog('[IDB] UPDATE β†’ Failed: key does not exist. Create first.');
return;
}
const updated: DemoDraft = {
...existing,
id: existing.id + 1,
content: `Updated at ${new Date().toLocaleTimeString()}`,
};
await demoIndexedDB.setItem(IDB_KEY, updated);
pushLog(`[IDB] UPDATE β†’ Persisted: ${JSON.stringify(updated)}`);
} catch (err) {
pushLog(`[IDB] UPDATE β†’ ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
}, [pushLog]);
const idbDelete = useCallback(async () => {
try {
await demoIndexedDB.removeItem(IDB_KEY);
setIdbResult('(deleted)');
pushLog(`[IDB] DELETE β†’ Removed key "${IDB_KEY}"`);
} catch (err) {
pushLog(`[IDB] DELETE β†’ ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
}, [pushLog]);
const idbClear = useCallback(async () => {
try {
await demoIndexedDB.clear();
setIdbResult('(cleared)');
pushLog('[IDB] CLEAR β†’ All IndexedDB entries removed');
} catch (err) {
pushLog(`[IDB] CLEAR β†’ ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
}, [pushLog]);
// ═══════════════════════════════════════════════════════════════
// ── Render ────────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════
return (
<div style={{ padding: 24, fontFamily: 'monospace', maxWidth: 900 }}>
<h2>πŸ” @repo/core-storage β€” Dual Backend CRUD Demo</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, marginTop: 16 }}>
{/* ── Left: localStorage ─────────────────────────────────── */}
<div>
<h3 style={{ color: '#22c55e' }}>πŸ“¦ localStorage (AES Encrypted)</h3>
<p style={{ color: '#888', fontSize: 12, marginBottom: 12 }}>
Key: <code>{LS_KEY}</code> β€” stored encrypted at rest
<br />
Verify: <strong>DevTools β†’ Application β†’ Local Storage</strong>
</p>
<CRUDButtons
actions={[
{ label: 'βž• Create', handler: lsCreate, color: '#22c55e' },
{ label: 'πŸ“– Read', handler: lsRead, color: '#3b82f6' },
{ label: '✏️ Update', handler: lsUpdate, color: '#f59e0b' },
{ label: 'πŸ—‘οΈ Delete', handler: lsDelete, color: '#ef4444' },
{ label: 'πŸ’£ Clear', handler: lsClear, color: '#6b7280' },
]}
/>
<pre style={preStyle}>{lsResult}</pre>
</div>
{/* ── Right: IndexedDB ───────────────────────────────────── */}
<div>
<h3 style={{ color: '#8b5cf6' }}>πŸ—ƒοΈ IndexedDB (app_db / kv_store)</h3>
<p style={{ color: '#888', fontSize: 12, marginBottom: 12 }}>
Key: <code>{IDB_KEY}</code> β€” plain JSON (not in ENCRYPTED_KEYS)
<br />
Verify: <strong>DevTools β†’ Application β†’ IndexedDB β†’ app_db</strong>
</p>
<CRUDButtons
actions={[
{ label: 'βž• Create', handler: idbCreate, color: '#8b5cf6' },
{ label: 'πŸ“– Read', handler: idbRead, color: '#06b6d4' },
{ label: '✏️ Update', handler: idbUpdate, color: '#f59e0b' },
{ label: 'πŸ—‘οΈ Delete', handler: idbDelete, color: '#ef4444' },
{ label: 'πŸ’£ Clear', handler: idbClear, color: '#6b7280' },
]}
/>
<pre style={preStyle}>{idbResult}</pre>
</div>
</div>
{/* ── Shared Action Log ────────────────────────────────────── */}
<h3 style={{ marginTop: 24 }}>πŸ“‹ Action Log</h3>
<div style={logContainerStyle}>
{log.length === 0 ? (
<span style={{ color: '#475569' }}>(no actions yet)</span>
) : (
log.map((entry, i) => <div key={i}>{entry}</div>)
)}
</div>
</div>
);
}