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:
Bernd Rodler
2026-08-07 22:10:26 +02:00
parent 0ac429fe36
commit cfdd091d22
29 changed files with 1068 additions and 93 deletions
+30 -4
View File
@@ -2,6 +2,8 @@ import { create } from 'zustand';
import type { CalendarTask } from '@/lib/jmap/types';
import type { IJMAPClient } from '@/lib/jmap/client-interface';
import { debug } from '@/lib/debug';
import { enqueueOperation, isNetworkError } from '@/lib/offline-write-queue';
import { useAccountStore } from '@/stores/account-store';
export type TaskViewFilter = 'all' | 'pending' | 'completed' | 'overdue';
@@ -58,10 +60,22 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
createTask: async (client, task) => {
debug.log('tasks', 'TaskStore/createTask', task);
const created = await client.createCalendarTask(task);
debug.log('tasks', 'TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
set({ tasks: [...get().tasks, created] });
return created;
try {
const created = await client.createCalendarTask(task);
debug.log('tasks', 'TaskStore/createTask result', { id: created.id, uid: created.uid, title: created.title });
set({ tasks: [...get().tasks, created] });
return created;
} catch (error) {
debug.error('TaskStore/createTask failed', error);
if (isNetworkError(error)) {
const accountId = useAccountStore.getState().activeAccountId;
if (accountId) {
enqueueOperation({ type: 'createTask', accountId, payload: task });
}
}
set({ error: 'Failed to create task' });
throw error;
}
},
updateTask: async (client, id, updates) => {
@@ -72,6 +86,12 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
});
} catch (error) {
debug.error('TaskStore/updateTask failed', error);
if (isNetworkError(error)) {
const accountId = useAccountStore.getState().activeAccountId;
if (accountId) {
enqueueOperation({ type: 'updateTask', accountId, payload: { id, updates } });
}
}
set({ error: 'Failed to update task' });
}
},
@@ -85,6 +105,12 @@ export const useTaskStore = create<TaskStore>((set, get) => ({
});
} catch (error) {
debug.error('TaskStore/deleteTask failed', error);
if (isNetworkError(error)) {
const accountId = useAccountStore.getState().activeAccountId;
if (accountId) {
enqueueOperation({ type: 'deleteTask', accountId, payload: { id } });
}
}
set({ error: 'Failed to delete task' });
}
},