From 934dab2325844ec146a18381c622953441c3b600 Mon Sep 17 00:00:00 2001 From: rowkav09 Date: Fri, 2 Oct 2026 02:07:33 +0100 Subject: [PATCH] Show a readable message for rejected server credentials Closes #1015 --- src/settings-onboarding-page.js | 2 +- test/settings-signin-readable-error.test.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 test/settings-signin-readable-error.test.js diff --git a/src/settings-onboarding-page.js b/src/settings-onboarding-page.js index 8d52da29..b453c0d8 100644 --- a/src/settings-onboarding-page.js +++ b/src/settings-onboarding-page.js @@ -13,7 +13,7 @@ let serverListLoaded = false; const label = { plex: 'Plex', jellyfin: 'Jellyfin', emby: 'Emby', navidrome: 'Navidrome' }; const say = (id, text) => { $(id).textContent = text; }; function li(text, button, action) { const row = document.createElement('li'); const span = document.createElement('span'); span.textContent = text; row.append(span); if (button) { const b = document.createElement('button'); b.type='button'; b.textContent=button; b.ariaLabel=button+" "+text; b.addEventListener('click', action); row.append(b); } return row; } -async function request(path, method='GET', data) { const r = await fetch(path, { method, cache:'no-store', headers:{Accept:'application/json',...(method==='GET'?{}:{'Content-Type':'application/json'})}, ...(data?{body:JSON.stringify(data)}:{...(method==='DELETE'?{body:'{}'}:{})}) }); const body = await r.json().catch(()=>({})); if (!r.ok) throw new Error(({too_many_servers:'You can add up to 8 servers.',invalid_server_url:'Enter a server address before signing in.',draft_update_failed:'Could not save the server. Try again.',credential_store_failed:'Could not save or restore the sign-in. Check the saved sign-in before trying again.'})[body.error] || body.error || 'request_failed'); return body; } +async function request(path, method='GET', data) { const r = await fetch(path, { method, cache:'no-store', headers:{Accept:'application/json',...(method==='GET'?{}:{'Content-Type':'application/json'})}, ...(data?{body:JSON.stringify(data)}:{...(method==='DELETE'?{body:'{}'}:{})}) }); const body = await r.json().catch(()=>({})); if (!r.ok) throw new Error(({authentication_failed:"That username or password didn't work.",too_many_servers:'You can add up to 8 servers.',invalid_server_url:'Enter a server address before signing in.',draft_update_failed:'Could not save the server. Try again.',credential_store_failed:'Could not save or restore the sign-in. Check the saved sign-in before trying again.'})[body.error] || body.error || 'request_failed'); return body; } async function load() { try { const state = await request('/api/settings/servers'); serverListLoaded = true; const list = $('servers-list'); const focusedServerAction = document.activeElement && list.contains(document.activeElement) ? document.activeElement.ariaLabel : null; list.replaceChildren(...(state.servers.length ? state.servers.map((server) => li((label[server.provider]||server.provider)+' - '+server.name+' - '+server.baseUrl, state.servers.length>1?'Remove':null, async () => { if (!confirm('Remove this server?')) return; try { const result = await request('/api/settings/servers','DELETE',{provider:server.provider,id:server.id}); await load(); say('discovery-state', result.tokenRemoved ? 'Server removed and its saved sign-in was deleted.' : 'Server removed, but its saved sign-in could not be deleted from this computer. Remove the nowplaying '+server.provider+':'+server.id+' credential in your operating system credential manager.'); } catch (e) { say('discovery-state','Could not remove: '+e.message); } })) : [li('No server connected yet.') ])); if (focusedServerAction) ([...list.querySelectorAll('button')].find(button => button.ariaLabel === focusedServerAction) || $('discover-servers')).focus(); if ($('first-run-state')) { if (state.activationFailed) activationError(); else if (state.configured) waitForActivation(); else say('first-run-state','Choose a discovered server or add one by address to get started.'); } } catch { if (!serverListLoaded) $('servers-list').replaceChildren(li('Server list unavailable.')); say('discovery-state','Could not load servers. Reload this page.'); } } let activating=false, activationStopped=false, activationAttempt=0, activationTimer=null; function activationError() { if (activationStopped) return; activationStopped=true; activating=false; say('first-run-state',"NowPlaying couldn't start after saving the server."); $('activation-recovery').hidden=false; } diff --git a/test/settings-signin-readable-error.test.js b/test/settings-signin-readable-error.test.js new file mode 100644 index 00000000..e4123a91 --- /dev/null +++ b/test/settings-signin-readable-error.test.js @@ -0,0 +1,9 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {runInNewContext} from 'node:vm'; +import {SERVER_SCRIPT} from '../src/settings-onboarding-page.js'; +test('server authentication failure gives a readable username/password error instead of an API code',async()=>{ + const start=SERVER_SCRIPT.indexOf('async function request('),end=SERVER_SCRIPT.indexOf('async function load()',start); + const context={fetch:async()=>({ok:false,json:async()=>({error:'authentication_failed'})})};runInNewContext(SERVER_SCRIPT.slice(start,end)+';globalThis.request=request;',context); + await assert.rejects(context.request('/api/setup/signin','POST',{}),{message:"That username or password didn't work."}); +});