fix(settings): enforce authoritative global image-gen policy and non-admin preference persistence

- Enforce authoritative administrator global image_gen_enabled disable in get_user_setting, do_generate_image, and image_editing_provider_available, preventing non-admin user preference overrides while preserving user-disabled opt-outs and can_generate_images privilege checks across direct, gallery, and editor paths.
- Filter non-admin preference persistence in saveImagePreferences to user preference keys only (image_model, image_edit_model, image_quality), preventing global policy toggles from leaking into user preferences.
- Return accurate 200 OK Response from saveImagePreferences after fallback persistence succeeds so settings UI displays 'Saved' instead of false 403 error, while retaining visibility of real errors.
- Load per-user preferences via readImagePreferences in initImageSettings so non-admin selections persist and display correctly after page reload.
- Clear per-user model preferences on successful admin save (POST /api/auth/settings) via PUT /api/prefs/{key} with value: null to prevent stale per-user preferences from shadowing updated global settings.
- Support clearing keys in PUT /api/prefs/{key} when value is null.
- Add comprehensive Python regression tests for global-disabled/user-enabled, global-enabled/user-disabled, and authorized generation, plus Node and browser regression tests for non-admin persistence, toggle isolation, reload, and admin clearing.
This commit is contained in:
Alexandre Teixeira
2026-10-31 20:35:44 +00:00
parent 1cb597ce12
commit 1888839e06
8 changed files with 467 additions and 11 deletions
+2 -3
View File
@@ -4,7 +4,7 @@
// them from the same places, so load order and init sequence are untouched.
import { byId as el } from './dom.js';
import { saveImagePreferences as _postSettings } from './imagePreferences.js';
import { saveImagePreferences as _postSettings, readImagePreferences } from './imagePreferences.js';
import { modelCaps } from '../editor/ai-models.js';
import { sortModelIds } from '../modelSort.js';
let preferencesListener = null;
@@ -46,8 +46,7 @@ export async function initImageSettings() {
}
} catch (e) { console.warn('Failed to load models for image settings', e); }
try {
const settingsRes = await fetch('/api/auth/settings', { credentials: 'same-origin' });
const settings = await settingsRes.json();
const settings = await readImagePreferences();
if (settings.image_model) modelSel.value = settings.image_model;
if (editModelSel) editModelSel.value = settings.image_edit_model || '';
if (settings.image_quality) qualSel.value = settings.image_quality;
+20 -3
View File
@@ -1,5 +1,7 @@
import { invalidateSettings } from '../appConfig.js';
const USER_PREFERENCE_KEYS = ['image_model', 'image_edit_model', 'image_quality'];
export async function readImagePreferences() {
const [prefsRes, settingsRes] = await Promise.all([
fetch('/api/prefs', { credentials: 'same-origin' }).catch(() => null),
@@ -12,6 +14,7 @@ export async function readImagePreferences() {
...settings,
image_model: prefs?.image_model || settings?.image_model || '',
image_edit_model: prefs?.image_edit_model || settings?.image_edit_model || '',
image_quality: prefs?.image_quality || settings?.image_quality || 'medium',
};
}
@@ -24,17 +27,31 @@ export function saveImagePreferences(patch) {
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch),
});
if (response.status === 403) {
for (const [key, value] of Object.entries(patch)) {
const userEntries = Object.entries(patch).filter(([k]) => USER_PREFERENCE_KEYS.includes(k));
for (const [key, value] of userEntries) {
const prefRes = await fetch(`/api/prefs/${encodeURIComponent(key)}`, {
method: 'PUT', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value }),
});
if (!prefRes.ok) {
throw new Error('Could not save image preferences');
const err = new Error(await prefRes.text().catch(() => 'Could not save image preferences'));
err.status = prefRes.status;
throw err;
}
}
} else if (!response.ok) {
response = typeof Response !== 'undefined'
? new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } })
: { ok: true, status: 200, json: async () => ({ ok: true }), text: async () => '{"ok":true}' };
} else if (response.ok) {
await Promise.all(USER_PREFERENCE_KEYS.map(key =>
fetch(`/api/prefs/${encodeURIComponent(key)}`, {
method: 'PUT', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value: null }),
}).catch(() => null)
));
} else {
throw new Error('Could not save image settings');
}
invalidateSettings();