diff --git a/locales/de.json b/locales/de.json index e0b905e5..c71b6e25 100644 --- a/locales/de.json +++ b/locales/de.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Civitai API Key", "civitaiApiKeyPlaceholder": "Geben Sie Ihren Civitai API Key ein", "civitaiApiKeyHelp": "Wird für die Authentifizierung beim Herunterladen von Modellen von Civitai verwendet", + "civitaiApiKeyConfigured": "Konfiguriert", + "civitaiApiKeyNotConfigured": "Nicht konfiguriert", + "civitaiApiKeySet": "Einrichten", "civitaiHost": { "label": "Civitai-Host", "help": "Wählen Sie aus, welche Civitai-Seite geöffnet wird, wenn Sie „View on Civitai“-Links verwenden.", diff --git a/locales/en.json b/locales/en.json index 9d47c8f1..c79f7ef8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Civitai API Key", "civitaiApiKeyPlaceholder": "Enter your Civitai API key", "civitaiApiKeyHelp": "Used for authentication when downloading models from Civitai", + "civitaiApiKeyConfigured": "Configured", + "civitaiApiKeyNotConfigured": "Not configured", + "civitaiApiKeySet": "Set up", "civitaiHost": { "label": "Civitai host", "help": "Choose which Civitai site opens when using View on Civitai links.", diff --git a/locales/es.json b/locales/es.json index 8883c1a7..06f91d23 100644 --- a/locales/es.json +++ b/locales/es.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Clave API de Civitai", "civitaiApiKeyPlaceholder": "Introduce tu clave API de Civitai", "civitaiApiKeyHelp": "Utilizada para autenticación al descargar modelos de Civitai", + "civitaiApiKeyConfigured": "Configurado", + "civitaiApiKeyNotConfigured": "No configurado", + "civitaiApiKeySet": "Configurar", "civitaiHost": { "label": "Host de Civitai", "help": "Elige qué sitio de Civitai se abre al usar los enlaces de \"View on Civitai\".", diff --git a/locales/fr.json b/locales/fr.json index 2f6d6290..df01f5fe 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -274,6 +274,9 @@ "civitaiApiKey": "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", + "civitaiApiKeyConfigured": "Configuré", + "civitaiApiKeyNotConfigured": "Non configuré", + "civitaiApiKeySet": "Configurer", "civitaiHost": { "label": "Hôte Civitai", "help": "Choisissez quel site Civitai s'ouvre lorsque vous utilisez les liens « View on Civitai ».", diff --git a/locales/he.json b/locales/he.json index 74f230e7..0ca63b59 100644 --- a/locales/he.json +++ b/locales/he.json @@ -274,6 +274,9 @@ "civitaiApiKey": "מפתח API של Civitai", "civitaiApiKeyPlaceholder": "הזן את מפתח ה-API שלך מ-Civitai", "civitaiApiKeyHelp": "משמש לאימות בעת הורדת מודלים מ-Civitai", + "civitaiApiKeyConfigured": "מוגדר", + "civitaiApiKeyNotConfigured": "לא מוגדר", + "civitaiApiKeySet": "הגדר", "civitaiHost": { "label": "מארח Civitai", "help": "בחר איזה אתר של Civitai ייפתח בעת שימוש בקישורי \"View on Civitai\".", diff --git a/locales/ja.json b/locales/ja.json index b6384dd8..f3cce870 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Civitai APIキー", "civitaiApiKeyPlaceholder": "Civitai APIキーを入力してください", "civitaiApiKeyHelp": "Civitaiからモデルをダウンロードするときの認証に使用されます", + "civitaiApiKeyConfigured": "設定済み", + "civitaiApiKeyNotConfigured": "未設定", + "civitaiApiKeySet": "設定", "civitaiHost": { "label": "Civitai ホスト", "help": "「View on Civitai」リンクを使うときに開く Civitai サイトを選択します。", diff --git a/locales/ko.json b/locales/ko.json index 25f54734..efdcc75e 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Civitai API 키", "civitaiApiKeyPlaceholder": "Civitai API 키를 입력하세요", "civitaiApiKeyHelp": "Civitai에서 모델을 다운로드할 때 인증에 사용됩니다", + "civitaiApiKeyConfigured": "설정됨", + "civitaiApiKeyNotConfigured": "설정되지 않음", + "civitaiApiKeySet": "설정", "civitaiHost": { "label": "Civitai 호스트", "help": "\"View on Civitai\" 링크를 사용할 때 어떤 Civitai 사이트를 열지 선택합니다.", diff --git a/locales/ru.json b/locales/ru.json index caab1fb6..bd8a188b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Ключ API Civitai", "civitaiApiKeyPlaceholder": "Введите ваш ключ API Civitai", "civitaiApiKeyHelp": "Используется для аутентификации при загрузке моделей с Civitai", + "civitaiApiKeyConfigured": "Настроен", + "civitaiApiKeyNotConfigured": "Не настроен", + "civitaiApiKeySet": "Настроить", "civitaiHost": { "label": "Хост Civitai", "help": "Выберите, какой сайт Civitai будет открываться при использовании ссылок «View on Civitai».", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 40ce0a5e..eb0353fb 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Civitai API 密钥", "civitaiApiKeyPlaceholder": "请输入你的 Civitai API 密钥", "civitaiApiKeyHelp": "用于从 Civitai 下载模型时的身份验证", + "civitaiApiKeyConfigured": "已配置", + "civitaiApiKeyNotConfigured": "未配置", + "civitaiApiKeySet": "设置", "civitaiHost": { "label": "Civitai 站点", "help": "选择使用“在 Civitai 中查看”时默认打开的 Civitai 站点。", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 556d2c9b..087a7e69 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -274,6 +274,9 @@ "civitaiApiKey": "Civitai API 金鑰", "civitaiApiKeyPlaceholder": "請輸入您的 Civitai API 金鑰", "civitaiApiKeyHelp": "用於從 Civitai 下載模型時的身份驗證", + "civitaiApiKeyConfigured": "已設定", + "civitaiApiKeyNotConfigured": "未設定", + "civitaiApiKeySet": "設定", "civitaiHost": { "label": "Civitai 站點", "help": "選擇使用「在 Civitai 中查看」時預設開啟的 Civitai 站點。", diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index 794d5de5..df253269 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -1328,6 +1328,9 @@ class SettingsHandler: "folder_paths", "libraries", "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) if value is not None: 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) if settings_file: response_data["settings_file"] = settings_file diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index a7ad599c..35c887c3 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -134,6 +134,9 @@ class SettingsManager: self._template_path = ( 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._migrate_setting_keys() self._ensure_default_settings() @@ -165,6 +168,12 @@ class SettingsManager: self._original_disk_payload = copy.deepcopy(data) if self._matches_template_payload(data): 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 except json.JSONDecodeError as exc: logger.error("Failed to parse settings.json: %s", exc) diff --git a/static/css/components/modal/settings-modal.css b/static/css/components/modal/settings-modal.css index 23d8ca7c..392fef71 100644 --- a/static/css/components/modal/settings-modal.css +++ b/static/css/components/modal/settings-modal.css @@ -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 { width: 100%; /* Take full width of parent */ position: relative; @@ -345,7 +350,7 @@ .api-key-input input { width: 100%; - padding: 6px 40px 6px 10px; /* Add left padding */ + padding: 6px 40px 6px 10px; /* Right padding for eye button */ height: 32px; box-sizing: border-box; border-radius: var(--border-radius-xs); @@ -353,6 +358,13 @@ background-color: var(--lora-surface); color: var(--text-color); 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 { @@ -364,12 +376,98 @@ opacity: 0.6; cursor: pointer; padding: 4px 8px; + transition: opacity 0.2s ease; } .api-key-input .toggle-visibility:hover { 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 { width: 100%; diff --git a/static/js/managers/DoctorManager.js b/static/js/managers/DoctorManager.js index 4e9407e2..7b954e2c 100644 --- a/static/js/managers/DoctorManager.js +++ b/static/js/managers/DoctorManager.js @@ -327,10 +327,15 @@ export class DoctorManager { case 'open-settings': modalManager.showModal('settingsModal'); window.setTimeout(() => { - const input = document.getElementById('civitaiApiKey'); - if (input) { - input.focus(); - input.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // Open the API key editor directly + if (typeof settingsManager.editApiKey === 'function') { + settingsManager.editApiKey(); + } else { + const input = document.getElementById('civitaiApiKey'); + if (input) { + input.focus(); + input.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } } }, 100); break; diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 945091fe..30763129 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -347,9 +347,9 @@ export class SettingsManager { if (this.isOpen) { this.loadSettingsToUI(); } else { - // Clear sensitive fields on close to prevent browser save-password prompts - const apiKeyInput = document.getElementById('civitaiApiKey'); - if (apiKeyInput) apiKeyInput.value = ''; + // Reset API key edit mode on close + this.cancelEditApiKey(true); + // Clear proxy password on close const proxyPasswordInput = document.getElementById('proxyPassword'); if (proxyPasswordInput) proxyPasswordInput.value = ''; } @@ -825,10 +825,8 @@ export class SettingsManager { usePortableCheckbox.checked = !!state.global.settings.use_portable_settings; } - const civitaiApiKeyInput = document.getElementById('civitaiApiKey'); - if (civitaiApiKeyInput) { - civitaiApiKeyInput.value = state.global.settings.civitai_api_key || ''; - } + // Update API key status display (do NOT pre-fill the input) + this.updateApiKeyStatus(); const civitaiHostSelect = document.getElementById('civitaiHost'); 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 = ' ' + + 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 = ' ' + + 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) { const input = button.parentElement.querySelector('input'); + if (!input) return; const icon = button.querySelector('i'); - - if (input.type === 'password') { + if (input.dataset.mask === 'css') { + // 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'; - icon.className = 'fas fa-eye-slash'; + if (icon) icon.className = 'fas fa-eye-slash'; } else { input.type = 'password'; - icon.className = 'fas fa-eye'; + if (icon) icon.className = 'fas fa-eye'; } } diff --git a/static/js/state/index.js b/static/js/state/index.js index e26c4b1a..e395bb39 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -5,6 +5,7 @@ import { DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/co const DEFAULT_SETTINGS_BASE = Object.freeze({ civitai_api_key: '', + civitai_api_key_set: false, civitai_host: 'civitai.com', download_backend: 'python', aria2c_path: '', diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 08bbb933..63d52d3b 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -95,21 +95,36 @@