feat: implement core-storage package with unified promise-based interface and encrypted-at-rest support
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user