refactor: move logout/change-password UI from floating widget to settings

The injected floating buttons overlapped the mobile composer send button.
Replace the tapIndex-injected widget with a proper client plugin
(panel-auth-ui, ui/ package): registers an account & security section in
the panel settings via the settings.section slot, with change-password
form and logout action. Host plugin keeps only the /panel-auth/change-
password and /panel-auth/logout endpoints.

- ui/: ModuleLoader-format client bundle (dsh.client declaration,
  exports ./client and ./package.json), host no-op stub, bare-package
  symlink required in the profile node_modules (documented in README).
- Host: renderAuthWidget/injectWidget/tapIndex injection removed.
- Tests: widget tests removed; change-password flows unchanged.
This commit is contained in:
dsh
2026-08-16 03:28:31 -04:00
parent f9883150e0
commit 96b5fb6ed0
6 changed files with 208 additions and 151 deletions
+1 -9
View File
@@ -6,7 +6,7 @@ import { strict as assert } from 'node:assert'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createGuard, installGuard, createAuditWriter, createLockout, renderLoginPage, renderAuthWidget, injectWidget } from './index.js'
import { createGuard, installGuard, createAuditWriter, createLockout, renderLoginPage } from './index.js'
import { hashPassword, verifyPassword } from './crypto.js'
const tmp = mkdtempSync(join(tmpdir(), 'panel-auth-test-'))
@@ -274,14 +274,6 @@ assert.ok(auditLines.some((e) => e.event === 'password-change'), 'audit missing
assert.ok(auditLines.some((e) => e.event === 'login-fail' && e.reason === 'password-change-old-mismatch'), 'audit missing old-mismatch entry')
assert.equal(auditLines.every((e) => typeof e.ip === 'string' && typeof e.ts === 'string'), true)
// 11b. widget rendering and index injection
const widget = renderAuthWidget({ logoutPath: '/panel-auth/logout', changePasswordPath: '/panel-auth/change-password' })
assert.ok(widget.includes('id="pna-widget"') && widget.includes('id="pna-logout"') && widget.includes('id="pna-change"') && widget.includes('pna-modal'))
const injected = injectWidget('<html><head></head><body>hi</body></html>', widget)
assert.ok(injected.includes('id="pna-widget"'))
assert.ok(injected.indexOf('id="pna-widget"') < injected.indexOf('</body>'))
assert.equal(injectWidget('<html>nobody</html>', widget), '<html>nobody</html>')
// 12. XSS: hostile `next` value is escaped in the page
const hostile = renderLoginPage({ realm: 'T', next: '/" onmouseover="alert(1)', error: '' })
assert.ok(!hostile.includes('onmouseover="alert(1)'), 'next not escaped')