feat: Phase 3+4 — security hardening + polish + offline + Electron push
Phase 3 (security): - P3.1: Feature gate server-side enforcement (403 on disabled features) - P3.2: Unified auth error interceptor (401→logout) - P3.3: Store-level state isolation via StoreSnapshot contract (added message-list-tabs + task stores to snapshot/restore cycle) - P3.4: Push event bus extraction — email-store no longer imports calendar/contact/filter/file stores directly - P1.3: Auth localStorage AES-GCM encryption via custom Zustand adapter Phase 4 (polish): - P4.1: Offline write queue — pending operations in localStorage, auto-retry on reconnect, offline-queue-indicator banner - P4.2: Identity spoofing — fromOverrideEmail domain validation - P4.3: WebSocket push for Electron via main-process IPC bridge (ws package with Authorization headers)
This commit is contained in:
@@ -11,18 +11,26 @@ import { useFilterStore } from '@/stores/filter-store';
|
||||
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
||||
import { useIdentityStore } from '@/stores/identity-store';
|
||||
import { useVacationStore } from '@/stores/vacation-store';
|
||||
import { useMessageListTabsStore } from '@/stores/message-list-tabs-store';
|
||||
import { useTaskStore } from '@/stores/task-store';
|
||||
|
||||
export interface StoreSnapshot<S> {
|
||||
snapshot: () => Partial<S>;
|
||||
clear: () => Partial<S>;
|
||||
}
|
||||
|
||||
// Minimal snapshot shapes - we only capture what we need
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type StoreSnapshot = Record<string, any>;
|
||||
type StoreData = Record<string, any>;
|
||||
|
||||
interface AccountSnapshot {
|
||||
email: StoreSnapshot;
|
||||
contact: StoreSnapshot;
|
||||
calendar: StoreSnapshot;
|
||||
filter: StoreSnapshot;
|
||||
identity: StoreSnapshot;
|
||||
vacation: StoreSnapshot;
|
||||
email: StoreData;
|
||||
contact: StoreData;
|
||||
calendar: StoreData;
|
||||
filter: StoreData;
|
||||
identity: StoreData;
|
||||
vacation: StoreData;
|
||||
messageListTabs: StoreData;
|
||||
tasks: StoreData;
|
||||
}
|
||||
|
||||
const cache = new Map<string, AccountSnapshot>();
|
||||
@@ -35,11 +43,9 @@ export function snapshotAccount(accountId: string): void {
|
||||
const filterState = useFilterStore.getState();
|
||||
const identityState = useIdentityStore.getState();
|
||||
const vacationState = useVacationStore.getState();
|
||||
const messageListTabsState = useMessageListTabsStore.getState();
|
||||
const taskState = useTaskStore.getState();
|
||||
|
||||
// Copy the captured collections so the snapshot is decoupled from the live
|
||||
// store: a later in-place mutation (e.g. an array push/splice, or stamping
|
||||
// fields onto a shared email object) must not retroactively corrupt a
|
||||
// snapshot taken earlier.
|
||||
cache.set(accountId, {
|
||||
email: {
|
||||
emails: [...emailState.emails],
|
||||
@@ -73,6 +79,17 @@ export function snapshotAccount(accountId: string): void {
|
||||
isEnabled: vacationState.isEnabled,
|
||||
isSupported: vacationState.isSupported,
|
||||
},
|
||||
messageListTabs: {
|
||||
registrations: { ...messageListTabsState.registrations },
|
||||
tabs: [...messageListTabsState.tabs],
|
||||
activeTabId: messageListTabsState.activeTabId,
|
||||
},
|
||||
tasks: {
|
||||
tasks: [...taskState.tasks],
|
||||
selectedTaskId: taskState.selectedTaskId,
|
||||
filter: taskState.filter,
|
||||
showCompleted: taskState.showCompleted,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,6 +115,8 @@ export function restoreAccount(accountId: string): boolean {
|
||||
useFilterStore.setState(snapshot.filter);
|
||||
useIdentityStore.setState(snapshot.identity);
|
||||
useVacationStore.setState(snapshot.vacation);
|
||||
useMessageListTabsStore.setState(snapshot.messageListTabs);
|
||||
useTaskStore.setState(snapshot.tasks);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -132,6 +151,8 @@ export function clearAllStores(): void {
|
||||
useVacationStore.getState().clearState();
|
||||
useCalendarStore.getState().clearState();
|
||||
useFilterStore.getState().clearState();
|
||||
useMessageListTabsStore.getState().clearState();
|
||||
useTaskStore.getState().clearTasks();
|
||||
}
|
||||
|
||||
/** Evict cached state for one account */
|
||||
|
||||
Reference in New Issue
Block a user