File size: 4,824 Bytes
4b3b11c 335f515 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | /**
* userScopedStorage β per-user localStorage namespacing.
*
* Industry best-practice for multi-account SaaS clients (mirrors the pattern
* used by ChatGPT, Claude Enterprise, and other production multi-tenant UIs):
* client-side UI state MUST be namespaced per authenticated user so that
* switching accounts never leaks the previous user's context into the new
* session. This module provides the minimum primitives to enforce that.
*
* Design
* ------
* - Feature modules (Chat, Projects, Imagine, Gallery, β¦) register a
* localStorage base key once, at module load.
* - AuthGate orchestrates the lifecycle:
* on login β restoreActiveStateForUser(user.id)
* on account switch β persistActiveStateForUser(prev.id)
* then restoreActiveStateForUser(next.id)
* on logout β persistActiveStateForUser(user.id)
* then clearActiveUiState()
*
* Physical layout in localStorage
* -------------------------------
* <baseKey> β current-session pointer (read by the UI)
* <baseKey>:user:<userId> β per-user saved copy
*
* The UI reads/writes the un-suffixed base key as before. Scoping happens
* only at login / logout / account-switch boundaries, so feature code does
* not need to thread a userId through every call site.
*
* Call-site helpers
* -----------------
* userScopedKey(base, userId) β compose a scoped key (for features that
* want to address scoped storage directly,
* e.g. hydrating state on mount).
* getCurrentUserId() β resolve the active user id from the
* persisted auth blob. Returns '' if
* unauthenticated.
*/
const USER_KEY_PREFIX = ':user:'
const AUTH_USER_LS_KEY = 'homepilot_auth_user'
const registry = new Set<string>()
/** Feature modules call this at module load to opt their key into per-user
* scoping. Idempotent. */
export function registerUserScopedKey(baseKey: string): void {
if (baseKey) registry.add(baseKey)
}
/** Inspect the current registry (primarily for tests / debugging). */
export function registeredUserScopedKeys(): readonly string[] {
return Array.from(registry)
}
/** Compose the per-user scoped key. */
export function userScopedKey(baseKey: string, userId: string): string {
return `${baseKey}${USER_KEY_PREFIX}${userId}`
}
/** Resolve the active user id from persisted auth state. '' if none. */
export function getCurrentUserId(): string {
try {
const raw = localStorage.getItem(AUTH_USER_LS_KEY)
if (!raw) return ''
const parsed = JSON.parse(raw)
return (parsed && typeof parsed.id === 'string') ? parsed.id : ''
} catch {
return ''
}
}
/** Save every registered global pointer into the user's scoped slot.
* Called when a user is about to stop being the active session
* (logout, account switch). */
export function persistActiveStateForUser(userId: string): void {
if (!userId) return
for (const base of registry) {
try {
const current = localStorage.getItem(base)
if (current !== null) {
localStorage.setItem(userScopedKey(base, userId), current)
}
} catch {
// localStorage can throw (quota, private mode). Non-fatal.
}
}
}
/** Restore the user's previously-saved pointers into the global slots used
* by the UI. If the user has no saved state, the global slot is cleared
* so the incoming session starts clean. */
export function restoreActiveStateForUser(userId: string): void {
if (!userId) return
for (const base of registry) {
try {
const saved = localStorage.getItem(userScopedKey(base, userId))
if (saved !== null) {
localStorage.setItem(base, saved)
} else {
localStorage.removeItem(base)
}
} catch {
// Non-fatal.
}
}
}
/** Wipe every registered global pointer. Used at logout, after the outgoing
* user's state has already been persisted into their scoped slot. */
export function clearActiveUiState(): void {
for (const base of registry) {
try { localStorage.removeItem(base) } catch { /* non-fatal */ }
}
}
// ββ Pre-registered application-wide pointers ββββββββββββββββββββββββββββββββ
// These are the UI state keys that must never bleed across accounts. Feature
// modules may still register additional keys at their own module load.
registerUserScopedKey('homepilot_conversation')
registerUserScopedKey('homepilot_current_project')
registerUserScopedKey('homepilot_imagine_items')
registerUserScopedKey('homepilot_personality_id')
registerUserScopedKey('homepilot_voice_style_id')
|