bind saves to loaded profile to prevent cross-profile clobber

This commit is contained in:
Eduard Gert
2026-06-12 12:19:30 +02:00
parent 860be01ebe
commit 29a165a586
@@ -54,28 +54,36 @@ export const useAutostartSetting = () => {
return ctx; return ctx;
}; };
type LoadedConfig = { profileName: string; data: Config };
const useSettingsState = () => { const useSettingsState = () => {
const { username, activeProfile, loaded: profileLoaded } = useProfile(); const { username, activeProfile, loaded: profileLoaded } = useProfile();
const [config, setConfig] = useState<Config | null>(null); const [loaded, setLoaded] = useState<LoadedConfig | null>(null);
const [guiVersion, setGuiVersion] = useState<string>("—"); const [guiVersion, setGuiVersion] = useState<string>("—");
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { useEffect(() => {
if (!profileLoaded || !activeProfile) return; if (!profileLoaded || !activeProfile) return;
let cancelled = false;
(async () => { (async () => {
try { try {
const c = await SettingsSvc.GetConfig({ const data = await SettingsSvc.GetConfig({
profileName: activeProfile, profileName: activeProfile,
username, username,
}); });
setConfig(c); if (cancelled) return;
setLoaded({ profileName: activeProfile, data });
} catch (e) { } catch (e) {
if (cancelled) return;
await errorDialog({ await errorDialog({
Title: i18next.t("settings.error.loadTitle"), Title: i18next.t("settings.error.loadTitle"),
Message: errorMessage(e), Message: errorMessage(e),
}); });
} }
})(); })();
return () => {
cancelled = true;
};
}, [profileLoaded, activeProfile, username]); }, [profileLoaded, activeProfile, username]);
useEffect(() => { useEffect(() => {
@@ -96,14 +104,14 @@ const useSettingsState = () => {
); );
const save = useCallback( const save = useCallback(
async (next: Config) => { async (profileName: string, next: Config) => {
// Sending the "**********" PSK mask back corrupts the stored PSK (wgtypes.ParseKey fails next connect). // Sending the "**********" PSK mask back corrupts the stored PSK (wgtypes.ParseKey fails next connect).
const { preSharedKey, ...rest } = next; const { preSharedKey, ...rest } = next;
try { try {
await SettingsSvc.SetConfig({ await SettingsSvc.SetConfig({
...rest, ...rest,
...(preSharedKey === "**********" ? {} : { preSharedKey }), ...(preSharedKey === "**********" ? {} : { preSharedKey }),
profileName: activeProfile, profileName,
username, username,
}); });
} catch (e) { } catch (e) {
@@ -113,62 +121,62 @@ const useSettingsState = () => {
}); });
} }
}, },
[activeProfile, username], [username],
); );
const setField = useCallback( const setField = useCallback(
<K extends keyof Config>(k: K, v: Config[K]) => { <K extends keyof Config>(k: K, v: Config[K]) => {
setConfig((c) => { setLoaded((cur) => {
if (!c) return c; if (!cur) return cur;
const next = { ...c, [k]: v }; const next = { ...cur.data, [k]: v };
if (saveTimer.current) clearTimeout(saveTimer.current); if (saveTimer.current) clearTimeout(saveTimer.current);
saveTimer.current = setTimeout(() => { saveTimer.current = setTimeout(() => {
save(next).catch(logSaveError); save(cur.profileName, next).catch(logSaveError);
}, SAVE_DEBOUNCE_MS); }, SAVE_DEBOUNCE_MS);
return next; return { profileName: cur.profileName, data: next };
}); });
}, },
[save], [save],
); );
const saveNow = useCallback(async () => { const saveNow = useCallback(async () => {
if (!config) return; if (!loaded) return;
if (saveTimer.current) { if (saveTimer.current) {
clearTimeout(saveTimer.current); clearTimeout(saveTimer.current);
saveTimer.current = null; saveTimer.current = null;
} }
await save(config); await save(loaded.profileName, loaded.data);
}, [config, save]); }, [loaded, save]);
const saveField = useCallback( const saveField = useCallback(
async <K extends keyof Config>(k: K, v: Config[K]) => { async <K extends keyof Config>(k: K, v: Config[K]) => {
if (!config) return; if (!loaded) return;
if (saveTimer.current) { if (saveTimer.current) {
clearTimeout(saveTimer.current); clearTimeout(saveTimer.current);
saveTimer.current = null; saveTimer.current = null;
} }
const next = { ...config, [k]: v }; const next = { ...loaded.data, [k]: v };
setConfig(next); setLoaded({ profileName: loaded.profileName, data: next });
await save(next); await save(loaded.profileName, next);
}, },
[config, save], [loaded, save],
); );
const saveFields = useCallback( const saveFields = useCallback(
async (partial: Partial<Config>) => { async (partial: Partial<Config>) => {
if (!config) return; if (!loaded) return;
if (saveTimer.current) { if (saveTimer.current) {
clearTimeout(saveTimer.current); clearTimeout(saveTimer.current);
saveTimer.current = null; saveTimer.current = null;
} }
const next = { ...config, ...partial }; const next = { ...loaded.data, ...partial };
setConfig(next); setLoaded({ profileName: loaded.profileName, data: next });
await save(next); await save(loaded.profileName, next);
}, },
[config, save], [loaded, save],
); );
return { config, guiVersion, setField, saveField, saveFields, saveNow }; return { config: loaded?.data ?? null, guiVersion, setField, saveField, saveFields, saveNow };
}; };
export const SettingsProvider = ({ children }: { children: ReactNode }) => { export const SettingsProvider = ({ children }: { children: ReactNode }) => {