mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
feat: enhance global settings update with data loss prevention
- Added safeguards to prevent overwriting non-empty arrays with empty arrays during global settings updates, specifically for the 'projects' field. - Implemented logging for updates to assist in diagnosing accidental wipes of critical settings. - Updated tests to verify that projects are preserved during logout transitions and that theme changes are ignored if a project wipe is attempted. - Enhanced the settings synchronization logic to ensure safe handling during authentication state changes.
This commit is contained in:
@@ -94,6 +94,17 @@ export function waitForMigrationComplete(): Promise<void> {
|
||||
return migrationCompletePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset migration state when auth is lost (logout/session expired).
|
||||
* This ensures that on re-login, the sync hook properly waits for
|
||||
* fresh settings hydration before starting to sync.
|
||||
*/
|
||||
export function resetMigrationState(): void {
|
||||
migrationCompleted = false;
|
||||
migrationCompletePromise = null;
|
||||
migrationCompleteResolve = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse localStorage data into settings object
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ import { setItem } from '@/lib/storage';
|
||||
import { useAppStore, type ThemeMode, THEME_STORAGE_KEY } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { useAuthStore } from '@/store/auth-store';
|
||||
import { waitForMigrationComplete } from './use-settings-migration';
|
||||
import { waitForMigrationComplete, resetMigrationState } from './use-settings-migration';
|
||||
import type { GlobalSettings } from '@automaker/types';
|
||||
|
||||
const logger = createLogger('SettingsSync');
|
||||
@@ -98,9 +98,35 @@ export function useSettingsSync(): SettingsSyncState {
|
||||
const lastSyncedRef = useRef<string>('');
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
// If auth is lost (logout / session expired), immediately stop syncing and
|
||||
// reset initialization so we can safely re-init after the next login.
|
||||
useEffect(() => {
|
||||
if (!authChecked) return;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
if (syncTimeoutRef.current) {
|
||||
clearTimeout(syncTimeoutRef.current);
|
||||
syncTimeoutRef.current = null;
|
||||
}
|
||||
lastSyncedRef.current = '';
|
||||
isInitializedRef.current = false;
|
||||
|
||||
// Reset migration state so next login properly waits for fresh hydration
|
||||
resetMigrationState();
|
||||
|
||||
setState({ loaded: false, error: null, syncing: false });
|
||||
}
|
||||
}, [authChecked, isAuthenticated]);
|
||||
|
||||
// Debounced sync function
|
||||
const syncToServer = useCallback(async () => {
|
||||
try {
|
||||
// Never sync when not authenticated (prevents overwriting server settings during logout/login transitions)
|
||||
const auth = useAuthStore.getState();
|
||||
if (!auth.authChecked || !auth.isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState((s) => ({ ...s, syncing: true }));
|
||||
const api = getHttpApiClient();
|
||||
const appState = useAppStore.getState();
|
||||
@@ -215,7 +241,7 @@ export function useSettingsSync(): SettingsSyncState {
|
||||
|
||||
// Subscribe to store changes and sync to server
|
||||
useEffect(() => {
|
||||
if (!state.loaded) return;
|
||||
if (!state.loaded || !authChecked || !isAuthenticated) return;
|
||||
|
||||
// Subscribe to app store changes
|
||||
const unsubscribeApp = useAppStore.subscribe((newState, prevState) => {
|
||||
@@ -272,11 +298,11 @@ export function useSettingsSync(): SettingsSyncState {
|
||||
clearTimeout(syncTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [state.loaded, scheduleSyncToServer, syncNow]);
|
||||
}, [state.loaded, authChecked, isAuthenticated, scheduleSyncToServer, syncNow]);
|
||||
|
||||
// Best-effort flush on tab close / backgrounding
|
||||
useEffect(() => {
|
||||
if (!state.loaded) return;
|
||||
if (!state.loaded || !authChecked || !isAuthenticated) return;
|
||||
|
||||
const handleBeforeUnload = () => {
|
||||
// Fire-and-forget; may not complete in all browsers, but helps in Electron/webview
|
||||
@@ -296,7 +322,7 @@ export function useSettingsSync(): SettingsSyncState {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [state.loaded, syncNow]);
|
||||
}, [state.loaded, authChecked, isAuthenticated, syncNow]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -81,9 +81,23 @@ export const THEME_STORAGE_KEY = 'automaker:theme';
|
||||
*/
|
||||
export function getStoredTheme(): ThemeMode | null {
|
||||
const stored = getItem(THEME_STORAGE_KEY);
|
||||
if (stored) {
|
||||
return stored as ThemeMode;
|
||||
if (stored) return stored as ThemeMode;
|
||||
|
||||
// Backwards compatibility: older versions stored theme inside the Zustand persist blob.
|
||||
// We intentionally keep reading it as a fallback so users don't get a "default theme flash"
|
||||
// on login/logged-out pages if THEME_STORAGE_KEY hasn't been written yet.
|
||||
try {
|
||||
const legacy = getItem('automaker-storage');
|
||||
if (!legacy) return null;
|
||||
const parsed = JSON.parse(legacy) as { state?: { theme?: unknown } } | { theme?: unknown };
|
||||
const theme = (parsed as any)?.state?.theme ?? (parsed as any)?.theme;
|
||||
if (typeof theme === 'string' && theme.length > 0) {
|
||||
return theme as ThemeMode;
|
||||
}
|
||||
} catch {
|
||||
// Ignore legacy parse errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user