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:
@@ -12,6 +12,8 @@ import { generateUUID } from '@/lib/utils';
|
||||
import { apiFetch } from '@/lib/browser-navigation';
|
||||
import { BIRTHDAY_CALENDAR_ID } from '@/lib/birthday-calendar';
|
||||
import { getClientByLocalAccountId } from './client-registry';
|
||||
import { enqueueOperation, isNetworkError } from '@/lib/offline-write-queue';
|
||||
import { useAccountStore } from '@/stores/account-store';
|
||||
|
||||
/**
|
||||
* When the Pro shell aggregates calendars/events from every connected
|
||||
@@ -409,13 +411,13 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
|
||||
createEvent: async (client, event, sendSchedulingMessages) => {
|
||||
set({ error: null });
|
||||
let targetAccountId: string | undefined = event.accountId;
|
||||
const cleanEvent = sanitizeOutgoingCalendarEventData({ ...event });
|
||||
try {
|
||||
// Resolve shared calendar context from calendarIds. Also pin the
|
||||
// local account from the calendar so we route through that
|
||||
// server's client when in multi-account Pro mode.
|
||||
let targetAccountId = event.accountId;
|
||||
let localAccountId = event.localAccountId;
|
||||
const cleanEvent = sanitizeOutgoingCalendarEventData({ ...event });
|
||||
if (event.calendarIds) {
|
||||
const remapped: Record<string, boolean> = {};
|
||||
for (const calId of Object.keys(event.calendarIds)) {
|
||||
@@ -491,6 +493,12 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
return mappedCreated;
|
||||
} catch (error) {
|
||||
debug.error('Failed to create event:', error);
|
||||
if (isNetworkError(error)) {
|
||||
const accountId = targetAccountId || useAccountStore.getState().activeAccountId;
|
||||
if (accountId) {
|
||||
enqueueOperation({ type: 'createEvent', accountId, payload: cleanEvent });
|
||||
}
|
||||
}
|
||||
set({ error: 'Failed to create event' });
|
||||
return null;
|
||||
}
|
||||
@@ -498,11 +506,12 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
|
||||
updateEvent: async (client, id, updates, sendSchedulingMessages) => {
|
||||
set({ error: null });
|
||||
const storeEvent = get().events.find(e => e.id === id);
|
||||
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
||||
const targetAccountId = storeEvent?.accountId;
|
||||
const cleanUpdates = sanitizeOutgoingCalendarEventData({ ...updates });
|
||||
try {
|
||||
// Resolve shared event IDs and client-side expanded occurrence IDs
|
||||
const storeEvent = get().events.find(e => e.id === id);
|
||||
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
||||
const targetAccountId = storeEvent?.accountId;
|
||||
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
||||
debug.log('calendar', 'Calendar updateEvent', {
|
||||
storeId: id,
|
||||
@@ -513,7 +522,6 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
updateKeys: Object.keys(updates),
|
||||
});
|
||||
// Remap namespaced calendarIds back to original IDs
|
||||
const cleanUpdates = sanitizeOutgoingCalendarEventData({ ...updates });
|
||||
if (cleanUpdates.calendarIds) {
|
||||
const remapped: Record<string, boolean> = {};
|
||||
for (const [calId, v] of Object.entries(cleanUpdates.calendarIds)) {
|
||||
@@ -556,6 +564,12 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
// iMIP send here produced duplicate emails.
|
||||
} catch (error) {
|
||||
debug.error('Failed to update event:', error);
|
||||
if (isNetworkError(error)) {
|
||||
const accountId = targetAccountId || useAccountStore.getState().activeAccountId;
|
||||
if (accountId) {
|
||||
enqueueOperation({ type: 'updateEvent', accountId, payload: { id: realId, updates: cleanUpdates } });
|
||||
}
|
||||
}
|
||||
set({ error: 'Failed to update event' });
|
||||
throw error;
|
||||
}
|
||||
@@ -788,11 +802,11 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
|
||||
deleteEvent: async (client, id, sendSchedulingMessages) => {
|
||||
set({ error: null });
|
||||
const storeEvent = get().events.find(e => e.id === id);
|
||||
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
||||
const targetAccountId = storeEvent?.accountId;
|
||||
try {
|
||||
// Resolve shared event IDs and client-side expanded occurrence IDs
|
||||
const storeEvent = get().events.find(e => e.id === id);
|
||||
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
||||
const targetAccountId = storeEvent?.accountId;
|
||||
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
||||
// Cancellation emails (iTIP CANCEL) are sent by the server via the
|
||||
// `sendSchedulingMessages` argument on the destroy below - a manual
|
||||
@@ -811,6 +825,12 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
}));
|
||||
} catch (error) {
|
||||
debug.error('Failed to delete event:', error);
|
||||
if (isNetworkError(error)) {
|
||||
const accountId = targetAccountId || useAccountStore.getState().activeAccountId;
|
||||
if (accountId) {
|
||||
enqueueOperation({ type: 'deleteEvent', accountId, payload: realId });
|
||||
}
|
||||
}
|
||||
set({ error: 'Failed to delete event' });
|
||||
throw error;
|
||||
}
|
||||
@@ -1302,3 +1322,27 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
import { registerPushHandler } from '@/lib/push-event-bus';
|
||||
|
||||
registerPushHandler('Calendar', async (client) => {
|
||||
const store = useCalendarStore.getState();
|
||||
if (store.supportsCalendar) {
|
||||
store.fetchCalendars(client);
|
||||
}
|
||||
});
|
||||
|
||||
registerPushHandler('CalendarEvent', async (client) => {
|
||||
const store = useCalendarStore.getState();
|
||||
if (store.supportsCalendar) {
|
||||
const { dateRange, selectedCalendarIds } = store;
|
||||
if (dateRange && selectedCalendarIds.length > 0) {
|
||||
store.fetchEvents(client, dateRange.start, dateRange.end);
|
||||
}
|
||||
const { useTaskStore } = await import('./task-store');
|
||||
const taskStore = useTaskStore.getState();
|
||||
if (taskStore.tasks.length > 0 || store.viewMode === 'tasks') {
|
||||
taskStore.fetchTasks(client);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user