From 239a69dcc44a20854fad456161b07a2b995c1f9e Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Sun, 20 Sep 2026 19:44:06 +0200 Subject: [PATCH] UI - Prevent accidental change of LLM Key (#4473) --- .../blueprint/settings/__init__.py | 10 ++ .../settings/templates/settings_llm_tab.html | 13 +- changedetectionio/forms.py | 18 ++- .../static/js/global-settings.js | 12 ++ .../tests/test_llm_api_key_security.py | 136 ++++++++++++++++++ .../translations/cs/LC_MESSAGES/messages.po | 8 ++ .../translations/de/LC_MESSAGES/messages.po | 8 ++ .../en_GB/LC_MESSAGES/messages.po | 8 ++ .../en_US/LC_MESSAGES/messages.po | 8 ++ .../translations/es/LC_MESSAGES/messages.po | 8 ++ .../translations/fr/LC_MESSAGES/messages.po | 8 ++ .../translations/id/LC_MESSAGES/messages.po | 8 ++ .../translations/it/LC_MESSAGES/messages.po | 8 ++ .../translations/ja/LC_MESSAGES/messages.po | 8 ++ .../translations/ko/LC_MESSAGES/messages.po | 8 ++ changedetectionio/translations/messages.pot | 10 +- .../translations/pl/LC_MESSAGES/messages.po | 8 ++ .../pt_BR/LC_MESSAGES/messages.po | 8 ++ .../translations/ru/LC_MESSAGES/messages.po | 8 ++ .../translations/tr/LC_MESSAGES/messages.po | 8 ++ .../translations/uk/LC_MESSAGES/messages.po | 8 ++ .../translations/zh/LC_MESSAGES/messages.po | 8 ++ .../zh_Hant_TW/LC_MESSAGES/messages.po | 8 ++ 23 files changed, 329 insertions(+), 6 deletions(-) diff --git a/changedetectionio/blueprint/settings/__init__.py b/changedetectionio/blueprint/settings/__init__.py index 19962aea8..f6b062187 100644 --- a/changedetectionio/blueprint/settings/__init__.py +++ b/changedetectionio/blueprint/settings/__init__.py @@ -238,6 +238,15 @@ def construct_blueprint(datastore: ChangeDetectionStore): ) llm_config = _get_llm_cfg(datastore) or {} llm_env_configured = llm_configured_via_env() + + # Once a key is stored, the provider and the key itself are locked and can only be + # cleared with "Remove provider". The key field renders blank so that an untouched + # save preserves it, which makes any stray value in it - a browser autofill, a bad + # paste - silently overwrite a working key on the next Save, with no copy kept + # anywhere to recover from. Disabling the input takes it out of the POST entirely; + # the form is seeded with data=default, so an absent field falls back to the stored + # value and the merge below is a no-op for it. + llm_provider_locked = bool(llm_config.get('api_key')) and not llm_env_configured llm_stored = datastore.data['settings']['application'].get('llm') or {} llm_token_budget_month = get_global_token_budget_month(datastore) llm_token_budget_month_env = get_global_token_budget_month() # env var only, for readonly logic @@ -253,6 +262,7 @@ def construct_blueprint(datastore: ChangeDetectionStore): api_key=datastore.data['settings']['application'].get('api_access_token'), llm_config=llm_config, llm_env_configured=llm_env_configured, + llm_provider_locked=llm_provider_locked, llm_stored=llm_stored, llm_token_budget_month=llm_token_budget_month, llm_token_budget_month_env=llm_token_budget_month_env, diff --git a/changedetectionio/blueprint/settings/templates/settings_llm_tab.html b/changedetectionio/blueprint/settings/templates/settings_llm_tab.html index 36a76997d..02a90e54b 100644 --- a/changedetectionio/blueprint/settings/templates/settings_llm_tab.html +++ b/changedetectionio/blueprint/settings/templates/settings_llm_tab.html @@ -118,7 +118,7 @@
- @@ -130,8 +130,13 @@
- {{ render_field(form.llm.form.api_key) }} - + {#- Disabled once a key is stored: a disabled input is not submitted at all, so there is + no way for an autofilled or mistyped value to reach the save path and overwrite a + working key. "Remove provider" is the only way to change it. -#} + {{ render_field(form.llm.form.api_key, disabled=llm_provider_locked) }} + + {%- if llm_provider_locked %}{{ _('Saved. Use "Remove provider" below to change the provider or key.') }}{% endif -%} +