From f96a0c5a18cf001657ae6d129e603ca5e82bd272 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Fri, 12 Jun 2026 11:34:11 +0900 Subject: [PATCH] fix(admin): address Copilot review findings - Reflect built-in defaults for boolean settings (ALLOW_PRIVATE_URLS defaults on) and allow clearing a saved boolean back to default, so the SSRF toggle matches actual runtime behavior. - Harden JSON loading: filter settings values to strings only, and schema-validate stored ADMIN_PROVIDERS entries, dropping malformed ones instead of letting them reach runtime code. - Set beforeunload returnValue so the unsaved-changes prompt shows in all browsers; reject non-finite numbers in settings validation. - Fix README/CN/JA docs that claimed the panel auto-generates AI_MODELS_CONFIG (providers are merged at read time, not written). - Add unit tests for corrupted-file value filtering and provider schema validation. --- README.md | 2 +- app/[lang]/admin/page.tsx | 2 ++ app/[lang]/admin/setting-field.tsx | 39 +++++++++++++++++++++++------- app/api/admin/settings/route.ts | 2 +- docs/cn/README_CN.md | 2 +- docs/ja/README_JA.md | 2 +- lib/admin/providers.ts | 8 +++++- lib/admin/settings-registry.ts | 5 ++++ lib/admin/settings.ts | 14 +++++++++-- lib/i18n/dictionaries/en.json | 1 + lib/i18n/dictionaries/ja.json | 1 + lib/i18n/dictionaries/zh-Hant.json | 1 + lib/i18n/dictionaries/zh.json | 1 + tests/unit/admin-providers.test.ts | 37 ++++++++++++++++++++++++++++ tests/unit/admin-settings.test.ts | 25 +++++++++++++++++++ 15 files changed, 126 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fe82103..1d9498e 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ Instead of hand-editing `.env`, you can manage server settings in a web admin pa 1. Set the `ADMIN_PASSWORD` environment variable (leave unset to disable the panel). 2. Visit `/admin` and sign in. -3. In the Models section, add providers with their API keys and model lists — the same UI as the in-app model settings. Saved models become server-side models available to all users, and credentials plus `AI_MODELS_CONFIG` are generated automatically. +3. In the Models section, add providers with their API keys and model lists — the same UI as the in-app model settings. Saved models become server-side models available to all users; they are stored in `data/settings.json` and merged with any `AI_MODELS_CONFIG` / `ai-models.json` from your environment at request time (the panel does not modify those env files). 4. Other sections cover access codes, generation parameters, features, observability, and quota. Saved settings are written to `data/settings.json` and apply immediately — no restart needed (a few settings such as Langfuse and DynamoDB are marked "Restart Required"). Precedence: settings saved in the panel override environment variables, which override built-in defaults. Removing a saved value falls back to the environment variable. diff --git a/app/[lang]/admin/page.tsx b/app/[lang]/admin/page.tsx index 097b480..cbf5bba 100644 --- a/app/[lang]/admin/page.tsx +++ b/app/[lang]/admin/page.tsx @@ -152,6 +152,8 @@ export default function AdminPage() { if (!hasDirty) return const handler = (e: BeforeUnloadEvent) => { e.preventDefault() + // Some browsers only show the prompt when returnValue is set + e.returnValue = "" } window.addEventListener("beforeunload", handler) return () => window.removeEventListener("beforeunload", handler) diff --git a/app/[lang]/admin/setting-field.tsx b/app/[lang]/admin/setting-field.tsx index 9340fb0..2676530 100644 --- a/app/[lang]/admin/setting-field.tsx +++ b/app/[lang]/admin/setting-field.tsx @@ -170,18 +170,39 @@ export function SettingField({ let control: React.ReactNode switch (def.type) { - case "boolean": + case "boolean": { + // When unset, reflect the built-in runtime default so the toggle + // matches actual behavior (e.g. ALLOW_PRIVATE_URLS defaults on). + const effective = + currentValue !== "" ? currentValue : (def.default ?? "false") + // A saved boolean can be cleared back to its env/default value. + const canClear = + (isDirty && pendingValue !== null) || source === "file" control = ( - - onChange(checked ? "true" : "false") - } - /> +
+ + onChange(checked ? "true" : "false") + } + /> + {canClear && !disabled && ( + + )} +
) break + } case "enum": control = (