From c35033b986d37a64687e48deb2f5a98b9e18c9a7 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:46:46 -0400 Subject: [PATCH] Keep the stored Unsplash key when the wallpaper is saved Open the API key field empty as a password input and send the key only when one is typed. Opening the field and saving no longer replaces the stored key with the row's label. --- e2e/wallpaper.spec.js | 48 +++++++++++++++++++++++++++++++++ ui/admin/index.html | 3 +-- ui/js/admin-settings.js | 59 +++++++++++++++++++++++++++-------------- ui/js/admin.js | 8 +++--- 4 files changed, 91 insertions(+), 27 deletions(-) diff --git a/e2e/wallpaper.spec.js b/e2e/wallpaper.spec.js index 45871396..d42fb187 100644 --- a/e2e/wallpaper.spec.js +++ b/e2e/wallpaper.spec.js @@ -5,6 +5,7 @@ const { test, expect } = require('@playwright/test'); const { seedConfig, readConfig, dismissSetupPrompt, setInlineRow } = require('./helpers'); +const { BASE_URL } = require('./base-url'); const WALLPAPER = { type: 'url', url: '/icons/wallpaper/none.png', brightness: 1, fit: 'fill' }; @@ -101,3 +102,50 @@ test('a failed wallpaper link goes back to the last saved wallpaper, not the one await expect(page.locator('#toast')).toHaveClass(/\berr\b/); await expect(page.locator('#ie-bgurl-v')).toContainText('saved-later.png'); }); + +test.describe('Unsplash key', () => { + const UNSPLASH = { type: 'unsplash', brightness: 0.62, collection: '' }; + + test.beforeEach(async ({ page, request }) => { + await seedConfig(request, { items: [], settings: { background: { ...UNSPLASH } } }); + const res = await request.post('/api/settings/unsplash-key', { + data: { apiKey: 'stored-key' }, + headers: { Origin: BASE_URL }, + }); + expect(res.ok()).toBeTruthy(); + await page.route('**/api/wallpaper', route => route.fulfill({ json: { url: null } })); + await page.goto('/admin/'); + await page.locator('.nl[data-sec="appearance"]').click(); + await expect(page.locator('#ie-apikey-v')).toHaveText('Configured'); + }); + + test('opening the field and saving does not send a key', async ({ page }) => { + const sent = []; + page.on('request', r => { + if (r.url().includes('/api/settings/unsplash-key') && r.method() === 'POST') sent.push(r.postData()); + }); + await page.locator('#ie-apikey .rv').click(); + const input = page.locator('#bg-apikey-inp'); + await expect(input).toHaveValue(''); + await expect(input).toHaveAttribute('type', 'password'); + await page.locator('#ie-apikey .rl').click(); + await expect(page.locator('#ie-apikey-v')).toHaveText('Configured'); + + await setInlineRow(page, 'ie-bgcol', 'bg-col-inp', 'AGVpqBZnzUE'); + const saved = page.waitForResponse(r => r.url().includes('/api/config') && r.request().method() === 'POST'); + await page.locator('#bg-save').click(); + expect((await saved).ok()).toBeTruthy(); + await expect(page.locator('#toast')).not.toHaveClass(/\berr\b/); + expect(sent).toEqual([]); + }); + + test('a typed key is sent once and the field is emptied', async ({ page }) => { + await setInlineRow(page, 'ie-apikey', 'bg-apikey-inp', 'new-key'); + await expect(page.locator('#ie-apikey-v')).toHaveText('New value set'); + const posted = page.waitForRequest(r => r.url().includes('/api/settings/unsplash-key') && r.method() === 'POST'); + await page.locator('#bg-save').click(); + expect(JSON.parse((await posted).postData() || '{}')).toEqual({ apiKey: 'new-key' }); + await expect(page.locator('#ie-apikey-v')).toHaveText('Configured'); + await expect(page.locator('#bg-apikey-inp')).toHaveValue(''); + }); +}); diff --git a/ui/admin/index.html b/ui/admin/index.html index 3497c55b..99c0825d 100644 --- a/ui/admin/index.html +++ b/ui/admin/index.html @@ -254,7 +254,6 @@

General

-
@@ -301,6 +300,6 @@

General

- + diff --git a/ui/js/admin-settings.js b/ui/js/admin-settings.js index 651d7bea..ce84ee6b 100644 --- a/ui/js/admin-settings.js +++ b/ui/js/admin-settings.js @@ -1,4 +1,4 @@ -import { toast, apiGet, apiPost, errorText, reveal, swapContent } from '/js/admin-shared.js?v=36dc5cc1'; +import { toast, apiGet, apiPost, errorText, reveal, swapContent, wireInlineEdit } from '/js/admin-shared.js?v=36dc5cc1'; import { socketProbeAdvice } from '/js/admin-error.js?v=b0881957'; import { pwStrength } from '/js/password-strength.js?v=389e0ed0'; import { t } from '/js/i18n.js?v=ed7c5565'; @@ -33,6 +33,7 @@ let _srvTrack = null; /** @type {{ dirty: () => boolean, reset: (force?: boolean) => void } | null} */ let _bgTrack = null; let _savedWallpaperUrl = ''; +let _unsplashKeySet = false; /** The wallpaper link the server holds, for undoing a link that failed. */ export const savedWallpaperUrl = () => _savedWallpaperUrl; @@ -71,7 +72,7 @@ const readWallpaperForm = () => _val('bg-url-inp', 'bg-url'), _val('bg-fit'), _val('bg-color-val'), - _val('bg-apikey-inp', 'bg-apikey'), + _val('bg-apikey-inp'), ]); /** Disables `buttonId` while its section matches what was last saved. */ @@ -170,22 +171,9 @@ export function loadSettings(c) { if (llEl) llEl.value = s.logLevel || 'info'; const langEl = inp('lang-sel'); if (langEl) langEl.value = s.language || 'en'; - /* The key itself is never included in /api/config. */ - const apiEl = inp('bg-apikey-inp') || inp('bg-apikey'); - if (apiEl) { - apiEl.placeholder = `●●●●●●●●●● (${t('common.configured')})`; - apiGet('/api/settings/unsplash-key') - .then(d => { - const vEl = el('ie-apikey-v'); - if (!d.configured) { - apiEl.placeholder = t('appearance.unsplashKeyPh'); - if (vEl) vEl.textContent = t('common.notSet'); - } else { - if (vEl) vEl.textContent = t('common.configured'); - } - }) - .catch(() => {}); - } + apiGet('/api/settings/unsplash-key') + .then(d => showUnsplashKeyState(!!d.configured)) + .catch(() => {}); const colEl = inp('bg-col'); if (colEl) colEl.value = bg.collection || ''; const urlEl = inp('bg-url'); @@ -333,6 +321,32 @@ export function showWallpaperFile(url) { } } +const unsplashKeyLabel = () => t(_unsplashKeySet ? 'common.configured' : 'common.notSet'); + +function showUnsplashKeyState(set) { + _unsplashKeySet = set; + const vEl = el('ie-apikey-v'); + if (vEl) vEl.textContent = unsplashKeyLabel(); +} + +/** The field opens empty. A blank field keeps the stored key. */ +export function wireUnsplashKey() { + const keyInp = document.createElement('input'); + keyInp.id = 'bg-apikey-inp'; + keyInp.autocomplete = 'new-password'; + document.body.appendChild(keyInp); + const row = el('ie-apikey'); + if (!row) return; + wireInlineEdit(row, keyInp, { + type: 'password', + placeholder: () => t(_unsplashKeySet ? 'widgetCfg.replaceSecret' : 'appearance.unsplashKeyPh'), + fill: false, + render: (valEl, v) => { + valEl.textContent = v ? t('widgetCfg.newValueSet') : unsplashKeyLabel(); + }, + }); +} + export function showBgFields(type) { const host = el('bg-unsplash-fields')?.parentElement; swapContent(host, () => { @@ -408,8 +422,13 @@ async function saveWallpaper() { /* After the main config. GET /api/config strips the key, so a config write that follows would overwrite it with nothing. */ if (type === 'unsplash') { - const keyVal = (inp('bg-apikey-inp') || inp('bg-apikey'))?.value?.trim() || ''; - if (keyVal) await apiPost('/api/settings/unsplash-key', { apiKey: keyVal }); + const keyEl = inp('bg-apikey-inp'); + const keyVal = keyEl?.value?.trim() || ''; + if (keyEl && keyVal) { + await apiPost('/api/settings/unsplash-key', { apiKey: keyVal }); + keyEl.value = ''; + showUnsplashKeyState(true); + } } _bgTrack?.reset(); toast(t('toast.saved')); diff --git a/ui/js/admin.js b/ui/js/admin.js index aa0a7c9b..2dfa2651 100644 --- a/ui/js/admin.js +++ b/ui/js/admin.js @@ -22,7 +22,8 @@ import { settingsDirty, showBgFields, showWallpaperFile, -} from '/js/admin-settings.js?v=65546081'; + wireUnsplashKey, +} from '/js/admin-settings.js?v=5d039019'; import { apiGet, apiPost, @@ -789,10 +790,7 @@ function initAllInlineEdits() { ); } - const apiInp = document.createElement('input'); - apiInp.id = 'bg-apikey-inp'; - document.body.appendChild(apiInp); - initInlineEdit('ie-apikey', 'bg-apikey-inp', { placeholder: () => t('appearance.unsplashKeyPh') }); + wireUnsplashKey(); const colInp = document.createElement('input'); colInp.id = 'bg-col-inp';