feat(settings): hide API key from frontend, use status+edit instead of password field

Backend changes:
- Add civitai_api_key to _NO_SYNC_KEYS, return only boolean civitai_api_key_set
- Clean up known template placeholder on load to prevent false positive

Frontend changes:
- Replace type=password with type=text + CSS masking (-webkit-text-security)
- Replace pre-filled input with status display (Configured/Not configured)
- Add inline edit view with Save/Cancel buttons
- Re-add eye toggle via CSS class toggle (not type switching)
- Use CSS transitions for smooth status/edit view switching

This prevents Chromium/Vivaldi password manager from triggering
'save password' prompts when opening the settings modal.
This commit is contained in:
Will Miao
2026-06-19 08:05:04 +08:00
parent faf64f8986
commit b24b1a7e57
19 changed files with 274 additions and 29 deletions

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Civitai API Key", "civitaiApiKey": "Civitai API Key",
"civitaiApiKeyPlaceholder": "Geben Sie Ihren Civitai API Key ein", "civitaiApiKeyPlaceholder": "Geben Sie Ihren Civitai API Key ein",
"civitaiApiKeyHelp": "Wird für die Authentifizierung beim Herunterladen von Modellen von Civitai verwendet", "civitaiApiKeyHelp": "Wird für die Authentifizierung beim Herunterladen von Modellen von Civitai verwendet",
"civitaiApiKeyConfigured": "Konfiguriert",
"civitaiApiKeyNotConfigured": "Nicht konfiguriert",
"civitaiApiKeySet": "Einrichten",
"civitaiHost": { "civitaiHost": {
"label": "Civitai-Host", "label": "Civitai-Host",
"help": "Wählen Sie aus, welche Civitai-Seite geöffnet wird, wenn Sie „View on Civitai“-Links verwenden.", "help": "Wählen Sie aus, welche Civitai-Seite geöffnet wird, wenn Sie „View on Civitai“-Links verwenden.",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Civitai API Key", "civitaiApiKey": "Civitai API Key",
"civitaiApiKeyPlaceholder": "Enter your Civitai API key", "civitaiApiKeyPlaceholder": "Enter your Civitai API key",
"civitaiApiKeyHelp": "Used for authentication when downloading models from Civitai", "civitaiApiKeyHelp": "Used for authentication when downloading models from Civitai",
"civitaiApiKeyConfigured": "Configured",
"civitaiApiKeyNotConfigured": "Not configured",
"civitaiApiKeySet": "Set up",
"civitaiHost": { "civitaiHost": {
"label": "Civitai host", "label": "Civitai host",
"help": "Choose which Civitai site opens when using View on Civitai links.", "help": "Choose which Civitai site opens when using View on Civitai links.",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Clave API de Civitai", "civitaiApiKey": "Clave API de Civitai",
"civitaiApiKeyPlaceholder": "Introduce tu clave API de Civitai", "civitaiApiKeyPlaceholder": "Introduce tu clave API de Civitai",
"civitaiApiKeyHelp": "Utilizada para autenticación al descargar modelos de Civitai", "civitaiApiKeyHelp": "Utilizada para autenticación al descargar modelos de Civitai",
"civitaiApiKeyConfigured": "Configurado",
"civitaiApiKeyNotConfigured": "No configurado",
"civitaiApiKeySet": "Configurar",
"civitaiHost": { "civitaiHost": {
"label": "Host de Civitai", "label": "Host de Civitai",
"help": "Elige qué sitio de Civitai se abre al usar los enlaces de \"View on Civitai\".", "help": "Elige qué sitio de Civitai se abre al usar los enlaces de \"View on Civitai\".",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Clé API Civitai", "civitaiApiKey": "Clé API Civitai",
"civitaiApiKeyPlaceholder": "Entrez votre clé API Civitai", "civitaiApiKeyPlaceholder": "Entrez votre clé API Civitai",
"civitaiApiKeyHelp": "Utilisée pour l'authentification lors du téléchargement de modèles depuis Civitai", "civitaiApiKeyHelp": "Utilisée pour l'authentification lors du téléchargement de modèles depuis Civitai",
"civitaiApiKeyConfigured": "Configuré",
"civitaiApiKeyNotConfigured": "Non configuré",
"civitaiApiKeySet": "Configurer",
"civitaiHost": { "civitaiHost": {
"label": "Hôte Civitai", "label": "Hôte Civitai",
"help": "Choisissez quel site Civitai s'ouvre lorsque vous utilisez les liens « View on Civitai ».", "help": "Choisissez quel site Civitai s'ouvre lorsque vous utilisez les liens « View on Civitai ».",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "מפתח API של Civitai", "civitaiApiKey": "מפתח API של Civitai",
"civitaiApiKeyPlaceholder": "הזן את מפתח ה-API שלך מ-Civitai", "civitaiApiKeyPlaceholder": "הזן את מפתח ה-API שלך מ-Civitai",
"civitaiApiKeyHelp": "משמש לאימות בעת הורדת מודלים מ-Civitai", "civitaiApiKeyHelp": "משמש לאימות בעת הורדת מודלים מ-Civitai",
"civitaiApiKeyConfigured": "מוגדר",
"civitaiApiKeyNotConfigured": "לא מוגדר",
"civitaiApiKeySet": "הגדר",
"civitaiHost": { "civitaiHost": {
"label": "מארח Civitai", "label": "מארח Civitai",
"help": "בחר איזה אתר של Civitai ייפתח בעת שימוש בקישורי \"View on Civitai\".", "help": "בחר איזה אתר של Civitai ייפתח בעת שימוש בקישורי \"View on Civitai\".",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Civitai APIキー", "civitaiApiKey": "Civitai APIキー",
"civitaiApiKeyPlaceholder": "Civitai APIキーを入力してください", "civitaiApiKeyPlaceholder": "Civitai APIキーを入力してください",
"civitaiApiKeyHelp": "Civitaiからモデルをダウンロードするときの認証に使用されます", "civitaiApiKeyHelp": "Civitaiからモデルをダウンロードするときの認証に使用されます",
"civitaiApiKeyConfigured": "設定済み",
"civitaiApiKeyNotConfigured": "未設定",
"civitaiApiKeySet": "設定",
"civitaiHost": { "civitaiHost": {
"label": "Civitai ホスト", "label": "Civitai ホスト",
"help": "「View on Civitai」リンクを使うときに開く Civitai サイトを選択します。", "help": "「View on Civitai」リンクを使うときに開く Civitai サイトを選択します。",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Civitai API 키", "civitaiApiKey": "Civitai API 키",
"civitaiApiKeyPlaceholder": "Civitai API 키를 입력하세요", "civitaiApiKeyPlaceholder": "Civitai API 키를 입력하세요",
"civitaiApiKeyHelp": "Civitai에서 모델을 다운로드할 때 인증에 사용됩니다", "civitaiApiKeyHelp": "Civitai에서 모델을 다운로드할 때 인증에 사용됩니다",
"civitaiApiKeyConfigured": "설정됨",
"civitaiApiKeyNotConfigured": "설정되지 않음",
"civitaiApiKeySet": "설정",
"civitaiHost": { "civitaiHost": {
"label": "Civitai 호스트", "label": "Civitai 호스트",
"help": "\"View on Civitai\" 링크를 사용할 때 어떤 Civitai 사이트를 열지 선택합니다.", "help": "\"View on Civitai\" 링크를 사용할 때 어떤 Civitai 사이트를 열지 선택합니다.",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Ключ API Civitai", "civitaiApiKey": "Ключ API Civitai",
"civitaiApiKeyPlaceholder": "Введите ваш ключ API Civitai", "civitaiApiKeyPlaceholder": "Введите ваш ключ API Civitai",
"civitaiApiKeyHelp": "Используется для аутентификации при загрузке моделей с Civitai", "civitaiApiKeyHelp": "Используется для аутентификации при загрузке моделей с Civitai",
"civitaiApiKeyConfigured": "Настроен",
"civitaiApiKeyNotConfigured": "Не настроен",
"civitaiApiKeySet": "Настроить",
"civitaiHost": { "civitaiHost": {
"label": "Хост Civitai", "label": "Хост Civitai",
"help": "Выберите, какой сайт Civitai будет открываться при использовании ссылок «View on Civitai».", "help": "Выберите, какой сайт Civitai будет открываться при использовании ссылок «View on Civitai».",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Civitai API 密钥", "civitaiApiKey": "Civitai API 密钥",
"civitaiApiKeyPlaceholder": "请输入你的 Civitai API 密钥", "civitaiApiKeyPlaceholder": "请输入你的 Civitai API 密钥",
"civitaiApiKeyHelp": "用于从 Civitai 下载模型时的身份验证", "civitaiApiKeyHelp": "用于从 Civitai 下载模型时的身份验证",
"civitaiApiKeyConfigured": "已配置",
"civitaiApiKeyNotConfigured": "未配置",
"civitaiApiKeySet": "设置",
"civitaiHost": { "civitaiHost": {
"label": "Civitai 站点", "label": "Civitai 站点",
"help": "选择使用“在 Civitai 中查看”时默认打开的 Civitai 站点。", "help": "选择使用“在 Civitai 中查看”时默认打开的 Civitai 站点。",

View File

@@ -274,6 +274,9 @@
"civitaiApiKey": "Civitai API 金鑰", "civitaiApiKey": "Civitai API 金鑰",
"civitaiApiKeyPlaceholder": "請輸入您的 Civitai API 金鑰", "civitaiApiKeyPlaceholder": "請輸入您的 Civitai API 金鑰",
"civitaiApiKeyHelp": "用於從 Civitai 下載模型時的身份驗證", "civitaiApiKeyHelp": "用於從 Civitai 下載模型時的身份驗證",
"civitaiApiKeyConfigured": "已設定",
"civitaiApiKeyNotConfigured": "未設定",
"civitaiApiKeySet": "設定",
"civitaiHost": { "civitaiHost": {
"label": "Civitai 站點", "label": "Civitai 站點",
"help": "選擇使用「在 Civitai 中查看」時預設開啟的 Civitai 站點。", "help": "選擇使用「在 Civitai 中查看」時預設開啟的 Civitai 站點。",

View File

@@ -1328,6 +1328,9 @@ class SettingsHandler:
"folder_paths", "folder_paths",
"libraries", "libraries",
"active_library", "active_library",
# Sensitive — never expose the actual value to the frontend;
# frontend receives a boolean instead (civitai_api_key_set).
"civitai_api_key",
} }
) )
@@ -1382,6 +1385,9 @@ class SettingsHandler:
value = self._settings.get(key) value = self._settings.get(key)
if value is not None: if value is not None:
response_data[key] = value response_data[key] = value
# Sensitive fields: only expose a boolean indicating whether set
raw_key = self._settings.get("civitai_api_key")
response_data["civitai_api_key_set"] = bool(raw_key)
settings_file = getattr(self._settings, "settings_file", None) settings_file = getattr(self._settings, "settings_file", None)
if settings_file: if settings_file:
response_data["settings_file"] = settings_file response_data["settings_file"] = settings_file

View File

@@ -134,6 +134,9 @@ class SettingsManager:
self._template_path = ( self._template_path = (
Path(__file__).resolve().parents[2] / "settings.json.example" Path(__file__).resolve().parents[2] / "settings.json.example"
) )
# Known placeholder value in settings.json.example; any file containing
# this value should be treated as "not configured".
self._TEMPLATE_PLACEHOLDER_API_KEY = "your_civitai_api_key_here"
self.settings = self._load_settings() self.settings = self._load_settings()
self._migrate_setting_keys() self._migrate_setting_keys()
self._ensure_default_settings() self._ensure_default_settings()
@@ -165,6 +168,12 @@ class SettingsManager:
self._original_disk_payload = copy.deepcopy(data) self._original_disk_payload = copy.deepcopy(data)
if self._matches_template_payload(data): if self._matches_template_payload(data):
self._preserve_disk_template = True self._preserve_disk_template = True
# Clean up the template placeholder so it is not treated
# as a real key (affects both the frontend boolean and
# the downloader's Authorization header).
placeholder = self._TEMPLATE_PLACEHOLDER_API_KEY
if data.get("civitai_api_key") == placeholder:
data["civitai_api_key"] = ""
return data return data
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
logger.error("Failed to parse settings.json: %s", exc) logger.error("Failed to parse settings.json: %s", exc)

View File

@@ -335,7 +335,12 @@
} }
} }
/* API key input specific styles */ /* API key input — CSS masking (prevents Chrome password manager triggers) */
.api-key-masked {
-webkit-text-security: disc;
}
/* API key input specific styles (shared with proxy password) */
.api-key-input { .api-key-input {
width: 100%; /* Take full width of parent */ width: 100%; /* Take full width of parent */
position: relative; position: relative;
@@ -345,7 +350,7 @@
.api-key-input input { .api-key-input input {
width: 100%; width: 100%;
padding: 6px 40px 6px 10px; /* Add left padding */ padding: 6px 40px 6px 10px; /* Right padding for eye button */
height: 32px; height: 32px;
box-sizing: border-box; box-sizing: border-box;
border-radius: var(--border-radius-xs); border-radius: var(--border-radius-xs);
@@ -353,6 +358,13 @@
background-color: var(--lora-surface); background-color: var(--lora-surface);
color: var(--text-color); color: var(--text-color);
font-size: 0.95em; font-size: 0.95em;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.api-key-input input:focus {
border-color: var(--lora-accent);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1);
} }
.api-key-input .toggle-visibility { .api-key-input .toggle-visibility {
@@ -364,12 +376,98 @@
opacity: 0.6; opacity: 0.6;
cursor: pointer; cursor: pointer;
padding: 4px 8px; padding: 4px 8px;
transition: opacity 0.2s ease;
} }
.api-key-input .toggle-visibility:hover { .api-key-input .toggle-visibility:hover {
opacity: 1; opacity: 1;
} }
/* API key item — stack status/edit views vertically for smooth cross-fade */
.api-key-item .setting-control {
flex-direction: column;
align-items: flex-end;
}
/* API key status display (shown when not editing) */
.api-key-status {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
justify-content: flex-end;
transition: opacity 0.2s ease, transform 0.2s ease, max-height 0.25s ease;
max-height: 80px;
overflow: hidden;
}
.api-key-status.is-hidden {
opacity: 0;
max-height: 0;
transform: translateY(-4px);
pointer-events: none;
margin: 0;
}
.api-key-status-text {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.95em;
white-space: nowrap;
transition: color 0.2s ease;
}
/* Status color modifiers — replace inline styles */
.api-key-status--configured .fa-check-circle {
color: var(--lora-success);
}
.api-key-status--unconfigured .fa-times-circle {
color: var(--lora-error);
}
/* Utility classes for status icon colors (used by JS) */
.text-success {
color: var(--lora-success);
}
.text-error {
color: var(--lora-error);
}
/* API key inline edit container — flex row with input + buttons */
.api-key-edit {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
justify-content: flex-end;
transition: opacity 0.2s ease, transform 0.2s ease, max-height 0.25s ease;
max-height: 80px;
overflow: hidden;
}
.api-key-edit.is-hidden {
opacity: 0;
max-height: 0;
transform: translateY(-4px);
pointer-events: none;
margin: 0;
}
.api-key-edit .api-key-input {
flex: 1;
min-width: 0;
}
.api-key-edit .primary-btn,
.api-key-edit .secondary-btn {
height: 32px;
flex-shrink: 0;
white-space: nowrap;
}
/* Text input wrapper styles for consistent input styling */ /* Text input wrapper styles for consistent input styling */
.text-input-wrapper { .text-input-wrapper {
width: 100%; width: 100%;

View File

@@ -327,10 +327,15 @@ export class DoctorManager {
case 'open-settings': case 'open-settings':
modalManager.showModal('settingsModal'); modalManager.showModal('settingsModal');
window.setTimeout(() => { window.setTimeout(() => {
const input = document.getElementById('civitaiApiKey'); // Open the API key editor directly
if (input) { if (typeof settingsManager.editApiKey === 'function') {
input.focus(); settingsManager.editApiKey();
input.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else {
const input = document.getElementById('civitaiApiKey');
if (input) {
input.focus();
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} }
}, 100); }, 100);
break; break;

View File

@@ -347,9 +347,9 @@ export class SettingsManager {
if (this.isOpen) { if (this.isOpen) {
this.loadSettingsToUI(); this.loadSettingsToUI();
} else { } else {
// Clear sensitive fields on close to prevent browser save-password prompts // Reset API key edit mode on close
const apiKeyInput = document.getElementById('civitaiApiKey'); this.cancelEditApiKey(true);
if (apiKeyInput) apiKeyInput.value = ''; // Clear proxy password on close
const proxyPasswordInput = document.getElementById('proxyPassword'); const proxyPasswordInput = document.getElementById('proxyPassword');
if (proxyPasswordInput) proxyPasswordInput.value = ''; if (proxyPasswordInput) proxyPasswordInput.value = '';
} }
@@ -825,10 +825,8 @@ export class SettingsManager {
usePortableCheckbox.checked = !!state.global.settings.use_portable_settings; usePortableCheckbox.checked = !!state.global.settings.use_portable_settings;
} }
const civitaiApiKeyInput = document.getElementById('civitaiApiKey'); // Update API key status display (do NOT pre-fill the input)
if (civitaiApiKeyInput) { this.updateApiKeyStatus();
civitaiApiKeyInput.value = state.global.settings.civitai_api_key || '';
}
const civitaiHostSelect = document.getElementById('civitaiHost'); const civitaiHostSelect = document.getElementById('civitaiHost');
if (civitaiHostSelect) { if (civitaiHostSelect) {
@@ -2898,16 +2896,97 @@ export class SettingsManager {
} }
} }
// ── CivitAI API Key management ──────────────────────────────
updateApiKeyStatus() {
const hasKey = !!(state.global.settings.civitai_api_key_set ||
state.global.settings.civitai_api_key);
const statusEl = document.getElementById('civitaiApiKeyStatus');
const statusText = document.getElementById('civitaiApiKeyStatusText');
const actionBtn = document.getElementById('civitaiApiKeyActionBtn');
if (!statusText || !actionBtn) return;
if (hasKey) {
statusText.classList.remove('api-key-status--unconfigured');
statusText.classList.add('api-key-status--configured');
statusText.innerHTML = '<i class="fas fa-check-circle text-success"></i> '
+ translate('settings.civitaiApiKeyConfigured', {}, 'Configured');
actionBtn.textContent = translate('common.actions.change', {}, 'Change');
} else {
statusText.classList.remove('api-key-status--configured');
statusText.classList.add('api-key-status--unconfigured');
statusText.innerHTML = '<i class="fas fa-times-circle text-error"></i> '
+ translate('settings.civitaiApiKeyNotConfigured', {}, 'Not configured');
actionBtn.textContent = translate('settings.civitaiApiKeySet', {}, 'Set up');
}
}
editApiKey() {
const statusEl = document.getElementById('civitaiApiKeyStatus');
if (statusEl) statusEl.classList.add('is-hidden');
const editContainer = document.getElementById('civitaiApiKeyEdit');
if (editContainer) editContainer.classList.remove('is-hidden');
// Focus the input
const input = document.getElementById('civitaiApiKey');
if (input) {
input.value = ''; // Never pre-fill the secret
setTimeout(() => input.focus(), 50);
}
}
cancelEditApiKey(silent) {
const editContainer = document.getElementById('civitaiApiKeyEdit');
if (editContainer) editContainer.classList.add('is-hidden');
const statusContainer = document.getElementById('civitaiApiKeyStatus');
if (statusContainer) statusContainer.classList.remove('is-hidden');
// Clear any typed value
const input = document.getElementById('civitaiApiKey');
if (input) input.value = '';
if (!silent) {
this.updateApiKeyStatus();
}
}
async saveApiKey() {
const input = document.getElementById('civitaiApiKey');
if (!input) return;
const value = input.value.trim();
try {
await this.saveSetting('civitai_api_key', value);
showToast('toast.settings.settingsUpdated',
{ setting: 'CivitAI API Key' }, 'success');
} catch (error) {
showToast('toast.settings.settingSaveFailed',
{ message: error.message }, 'error');
return;
}
// Update the in-memory flag so the UI reflects the change
state.global.settings.civitai_api_key_set = !!value;
this.cancelEditApiKey(true);
this.updateApiKeyStatus();
}
toggleInputVisibility(button) { toggleInputVisibility(button) {
const input = button.parentElement.querySelector('input'); const input = button.parentElement.querySelector('input');
if (!input) return;
const icon = button.querySelector('i'); const icon = button.querySelector('i');
if (input.dataset.mask === 'css') {
if (input.type === 'password') { // CSS-masked input (CivitAI API key) — toggle class, not type
input.classList.toggle('api-key-masked');
if (icon) {
icon.className = input.classList.contains('api-key-masked')
? 'fas fa-eye'
: 'fas fa-eye-slash';
}
} else if (input.type === 'password') {
input.type = 'text'; input.type = 'text';
icon.className = 'fas fa-eye-slash'; if (icon) icon.className = 'fas fa-eye-slash';
} else { } else {
input.type = 'password'; input.type = 'password';
icon.className = 'fas fa-eye'; if (icon) icon.className = 'fas fa-eye';
} }
} }

View File

@@ -5,6 +5,7 @@ import { DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/co
const DEFAULT_SETTINGS_BASE = Object.freeze({ const DEFAULT_SETTINGS_BASE = Object.freeze({
civitai_api_key: '', civitai_api_key: '',
civitai_api_key_set: false,
civitai_host: 'civitai.com', civitai_host: 'civitai.com',
download_backend: 'python', download_backend: 'python',
aria2c_path: '', aria2c_path: '',

View File

@@ -95,21 +95,36 @@
<div class="setting-item api-key-item"> <div class="setting-item api-key-item">
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<label for="civitaiApiKey">{{ t('settings.civitaiApiKey') }}</label> <label>{{ t('settings.civitaiApiKey') }}</label>
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.civitaiApiKeyHelp') }}"></i> <i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.civitaiApiKeyHelp') }}"></i>
</div> </div>
<div class="setting-control"> <div class="setting-control">
<div class="api-key-input"> <!-- Status display (shown when not editing) -->
<input type="password" <div id="civitaiApiKeyStatus" class="api-key-status">
id="civitaiApiKey" <span id="civitaiApiKeyStatusText" class="api-key-status-text api-key-status--unconfigured">
placeholder="{{ t('settings.civitaiApiKeyPlaceholder') }}" <i class="fas fa-times-circle text-error"></i>
autocomplete="new-password" {{ t('settings.civitaiApiKeyNotConfigured') }}
onblur="settingsManager.saveInputSetting('civitaiApiKey', 'civitai_api_key')" </span>
onkeydown="if(event.key === 'Enter') { this.blur(); }" /> <button type="button" class="secondary-btn" id="civitaiApiKeyActionBtn" onclick="settingsManager.editApiKey()">
<button class="toggle-visibility"> {{ t('settings.civitaiApiKeySet') }}
<i class="fas fa-eye"></i>
</button> </button>
</div> </div>
<!-- Inline edit view (shown when editing) -->
<div id="civitaiApiKeyEdit" class="api-key-edit is-hidden">
<div class="api-key-input">
<input type="text"
id="civitaiApiKey"
class="api-key-masked"
placeholder="{{ t('settings.civitaiApiKeyPlaceholder') }}"
autocomplete="off"
data-mask="css" />
<button type="button" class="toggle-visibility">
<i class="fas fa-eye"></i>
</button>
</div>
<button type="button" class="primary-btn" onclick="settingsManager.saveApiKey()">{{ t('common.actions.save') }}</button>
<button type="button" class="secondary-btn" onclick="settingsManager.cancelEditApiKey()">{{ t('common.actions.cancel') }}</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -26,7 +26,7 @@
'messages': list([ 'messages': list([
]), ]),
'settings': dict({ 'settings': dict({
'civitai_api_key': 'test-key', 'civitai_api_key_set': True,
'language': 'en', 'language': 'en',
'theme': 'dark', 'theme': 'dark',
}), }),

View File

@@ -134,8 +134,10 @@ async def test_get_settings_excludes_no_sync_keys():
assert payload["success"] is True assert payload["success"] is True
# Regular settings should be synced # Regular settings should be synced
assert payload["settings"]["civitai_api_key"] == "abc"
assert payload["settings"]["regular_setting"] == "value" assert payload["settings"]["regular_setting"] == "value"
# civitai_api_key is in _NO_SYNC_KEYS; only the boolean flag is returned
assert payload["settings"].get("civitai_api_key") is None
assert payload["settings"]["civitai_api_key_set"] is True
# _NO_SYNC_KEYS should not be synced # _NO_SYNC_KEYS should not be synced
assert "hash_chunk_size_mb" not in payload["settings"] assert "hash_chunk_size_mb" not in payload["settings"]
assert "folder_paths" not in payload["settings"] assert "folder_paths" not in payload["settings"]