feat: implement settings synchronization with server, including encryption and API endpoints

This commit is contained in:
Linus Rath
2026-03-11 16:22:47 +01:00
parent cf5f271be2
commit a8a1ad1d54
7 changed files with 311 additions and 0 deletions
+23
View File
@@ -7,6 +7,7 @@ import { useContactStore } from './contact-store';
import { useVacationStore } from './vacation-store';
import { useCalendarStore } from './calendar-store';
import { useFilterStore } from './filter-store';
import { useSettingsStore } from './settings-store';
import { debug } from '@/lib/debug';
import type { Identity } from '@/lib/jmap/types';
@@ -156,6 +157,11 @@ export const useAuthStore = create<AuthState>()(
error: null,
});
// Sync settings from server
useSettingsStore.getState().loadFromServer(username, serverUrl).finally(() => {
useSettingsStore.getState().enableSync(username, serverUrl);
});
if (rememberMe) {
try {
const res = await fetch('/api/auth/session', {
@@ -226,6 +232,11 @@ export const useAuthStore = create<AuthState>()(
scheduleRefresh(expires_in, get().refreshAccessToken);
// Sync settings from server
useSettingsStore.getState().loadFromServer(username, serverUrl).finally(() => {
useSettingsStore.getState().enableSync(username, serverUrl);
});
return true;
} catch (error) {
debug.error('OAuth login error:', error);
@@ -283,6 +294,8 @@ export const useAuthStore = create<AuthState>()(
clearRefreshTimer();
state.client?.disconnect();
useSettingsStore.getState().disableSync();
set({
isAuthenticated: false,
serverUrl: null,
@@ -365,6 +378,11 @@ export const useAuthStore = create<AuthState>()(
primaryIdentity,
accessToken: token,
});
// Sync settings from server
useSettingsStore.getState().loadFromServer(state.username || '', state.serverUrl).finally(() => {
useSettingsStore.getState().enableSync(state.username || '', state.serverUrl!);
});
return;
}
} catch (error) {
@@ -400,6 +418,11 @@ export const useAuthStore = create<AuthState>()(
primaryIdentity,
authMode: 'basic',
});
// Sync settings from server
useSettingsStore.getState().loadFromServer(username, serverUrl).finally(() => {
useSettingsStore.getState().enableSync(username, serverUrl);
});
return;
}
} catch (error) {
+75
View File
@@ -1,6 +1,15 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// Settings sync state (module-level, not persisted)
let syncEnabled = false;
let syncUsername: string | null = null;
let syncServerUrl: string | null = null;
let syncTimeout: ReturnType<typeof setTimeout> | null = null;
let isLoadingFromServer = false;
const SYNC_DEBOUNCE_MS = 2000;
export type FontSize = 'small' | 'medium' | 'large';
export type ListDensity = 'compact' | 'regular' | 'comfortable';
export type DeleteAction = 'trash' | 'permanent';
@@ -67,6 +76,11 @@ interface SettingsState {
addTrustedSender: (email: string) => void;
removeTrustedSender: (email: string) => void;
isSenderTrusted: (email: string) => boolean;
// Settings sync
enableSync: (username: string, serverUrl: string) => void;
disableSync: () => void;
loadFromServer: (username: string, serverUrl: string) => Promise<boolean>;
}
const DEFAULT_SETTINGS = {
@@ -227,6 +241,46 @@ export const useSettingsStore = create<SettingsState>()(
const normalizedEmail = email.toLowerCase().trim();
return get().trustedSenders.includes(normalizedEmail);
},
// Settings sync methods
enableSync: (username: string, serverUrl: string) => {
syncUsername = username;
syncServerUrl = serverUrl;
syncEnabled = true;
},
disableSync: () => {
syncEnabled = false;
syncUsername = null;
syncServerUrl = null;
if (syncTimeout) {
clearTimeout(syncTimeout);
syncTimeout = null;
}
},
loadFromServer: async (username: string, serverUrl: string) => {
try {
const res = await fetch('/api/settings', {
headers: {
'x-settings-username': username,
'x-settings-server': serverUrl,
},
});
if (!res.ok) return false;
const { settings } = await res.json();
if (settings && typeof settings === 'object') {
isLoadingFromServer = true;
get().importSettings(JSON.stringify(settings));
isLoadingFromServer = false;
return true;
}
return false;
} catch {
isLoadingFromServer = false;
return false;
}
},
}),
{
name: 'settings-storage',
@@ -277,4 +331,25 @@ if (typeof window !== 'undefined') {
applyFontSize(store.fontSize);
applyListDensity(store.listDensity);
applyAnimations(store.animationsEnabled);
// Auto-sync settings to server on any state change
useSettingsStore.subscribe(() => {
if (!syncEnabled || !syncUsername || !syncServerUrl || isLoadingFromServer) return;
if (syncTimeout) clearTimeout(syncTimeout);
syncTimeout = setTimeout(async () => {
try {
const settings = JSON.parse(useSettingsStore.getState().exportSettings());
const res = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: syncUsername, serverUrl: syncServerUrl, settings }),
});
if (res.status === 404) {
syncEnabled = false;
}
} catch {
// Silently ignore sync failures
}
}, SYNC_DEBOUNCE_MS);
});
}