Let an installation seed and lock user settings
The first two thirds of #207. A school wanting "warn about outside senders" on for three thousand pupils cannot ask three thousand pupils, and the reporter is right that this is a company policy rather than a preference. Two powers, and the difference between them is the whole request. `defaults` seed an account that has never had settings of its own and can be changed afterwards like anything else -- a starting point, not a rule. `enforced` are reapplied on every load and cannot be changed at all. Enforced controls stay visible and go dead, with a line saying why. The issue asked for that by name: a control that is simply missing reads as a bug to somebody who has used ihasmail without a policy. The lock is in the settings store rather than only on the controls. There is one door -- `update` -- and putting it there means an imported settings file, a settings file synced from a device that predates the policy, and a control somebody adds later and forgets to check are all covered by construction. Reset goes back to the installation's answer rather than to ihasmail's, so it cannot be a way around a policy either. Configured by environment variable or by a file, because ihasmail's own production runs read-only with no volume: an installation that cannot mount a file can still set a variable. Keys this build does not have are dropped, the same rule an imported settings file already gets -- a policy written against a newer ihasmail must not put a setting nothing reads into everybody's synced settings file. Malformed JSON stops the server rather than quietly doing nothing, since a policy that silently did not apply is indistinguishable from the feature not working. Tier three -- enforcing a setting once while still letting readers change it afterwards -- is not here. It needs a decision the reporter and I have not made yet, and it is the only part that stores anything new. Refs #207.
This commit is contained in:
@@ -4,6 +4,7 @@ import { hasCachedJson, loadJson, saveJson } from "@/lib/storage";
|
||||
import { effectiveMode, legacyTheme, migrateTheme, type Mode, type PaletteId } from "@/lib/palette";
|
||||
import type { SortLevel, SortPreset } from "@/lib/listSort";
|
||||
import { pendingSettingsKeys, queueSettingsPush } from "@/lib/settingsSync";
|
||||
import { policyDefaults, policyEnforced } from "@/lib/settingsPolicy";
|
||||
import { setDateTimePrefs, setUiLanguageForFormatting, type DateFormat, type TimeFormat } from "@/lib/datetime";
|
||||
import type { SwipeAction } from "@/lib/swipe";
|
||||
import { resolveUiLanguage } from "@/lib/languages";
|
||||
@@ -402,6 +403,14 @@ interface SettingsState {
|
||||
importJson(json: string): boolean;
|
||||
/** Apply the account's settings file over the cached ones. */
|
||||
hydrate(remote: Record<string, unknown>): void;
|
||||
/**
|
||||
* Seed an account that has never had settings of its own.
|
||||
*
|
||||
* Only for that case, which is why it is not `update`: these are a starting
|
||||
* point the reader may change, so applying them to somebody who already has
|
||||
* settings would be overwriting choices rather than defaulting them.
|
||||
*/
|
||||
seedFromPolicy(): void;
|
||||
}
|
||||
|
||||
const initialSettings = loadJson<Settings>("settings", DEFAULT_SETTINGS);
|
||||
@@ -428,7 +437,14 @@ export const useSettings = create<SettingsState>((set, get) => ({
|
||||
* change the theme cannot forget to update it and strand an older device
|
||||
* on a theme nobody picked.
|
||||
*/
|
||||
const merged = { ...get().settings, ...patch };
|
||||
/*
|
||||
* Enforcement lives here rather than only on the controls. The controls are
|
||||
* disabled and say why, which is the part a reader sees -- but a setting
|
||||
* the installation has decided must not be changeable through an imported
|
||||
* settings file, a keyboard shortcut, or a control somebody adds later and
|
||||
* forgets to check. There is one door, so the lock is on it. Issue #207.
|
||||
*/
|
||||
const merged = { ...get().settings, ...patch, ...policyEnforced() };
|
||||
const prefersDark = Boolean(window.matchMedia?.("(prefers-color-scheme: dark)").matches);
|
||||
const settings = { ...merged, theme: legacyTheme({ palette: merged.palette, mode: merged.mode }, prefersDark) };
|
||||
saveJson("settings", settings);
|
||||
@@ -442,13 +458,22 @@ export const useSettings = create<SettingsState>((set, get) => ({
|
||||
queueSettingsPush(syncedPart(settings));
|
||||
}
|
||||
},
|
||||
seedFromPolicy() {
|
||||
const defaults = policyDefaults();
|
||||
if (!Object.keys(defaults).length) return;
|
||||
get().update(defaults);
|
||||
},
|
||||
reset() {
|
||||
saveJson("settings", DEFAULT_SETTINGS);
|
||||
set({ settings: DEFAULT_SETTINGS });
|
||||
applyTheme(DEFAULT_SETTINGS);
|
||||
applyDateTimePrefs(DEFAULT_SETTINGS);
|
||||
applyLang(DEFAULT_SETTINGS);
|
||||
queueSettingsPush(syncedPart(DEFAULT_SETTINGS));
|
||||
/* Back to how this installation starts an account, not to how ihasmail
|
||||
starts one: resetting must not be a way around a policy, and the defaults
|
||||
an admin chose are the honest meaning of "reset" where there are any. */
|
||||
const base = { ...DEFAULT_SETTINGS, ...policyDefaults(), ...policyEnforced() };
|
||||
saveJson("settings", base);
|
||||
set({ settings: base });
|
||||
applyTheme(base);
|
||||
applyDateTimePrefs(base);
|
||||
applyLang(base);
|
||||
queueSettingsPush(syncedPart(base));
|
||||
},
|
||||
exportJson() {
|
||||
return JSON.stringify(get().settings, null, 2);
|
||||
@@ -463,7 +488,10 @@ export const useSettings = create<SettingsState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
hydrate(remote) {
|
||||
const settings = mergeRemote(get().settings, remote, pendingSettingsKeys());
|
||||
/* Enforced values win over what the account's own file says: a policy that
|
||||
an older sign-in has already written past would otherwise stay written
|
||||
past for ever. */
|
||||
const settings = { ...mergeRemote(get().settings, remote, pendingSettingsKeys()), ...policyEnforced() };
|
||||
// Cache it, so the next first frame on this browser is already right.
|
||||
saveJson("settings", settings);
|
||||
set({ settings });
|
||||
|
||||
Reference in New Issue
Block a user