forked from dsh/panel-auth
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:
@@ -404,137 +404,6 @@ export function renderLoginPage({ realm, next = '/', error = '', loginPath = DEF
|
||||
</html>`
|
||||
}
|
||||
|
||||
// ── in-panel auth widget (logout / change password) ─────────────────────────
|
||||
|
||||
/**
|
||||
* Self-contained widget injected into the panel's index.html via
|
||||
* webServer.tapIndex: two floating buttons (logout, change password) and a
|
||||
* change-password modal. Vanilla JS only, all styles scoped to `pna-*`.
|
||||
*/
|
||||
export function renderAuthWidget({ logoutPath, changePasswordPath }) {
|
||||
const logoutUrl = JSON.stringify(logoutPath)
|
||||
const changeUrl = JSON.stringify(changePasswordPath)
|
||||
return `<style>
|
||||
#pna-widget { position: fixed; right: 18px; bottom: 18px; z-index: 2147483000; display: flex; flex-direction: column; gap: 8px; }
|
||||
.pna-btn { display: flex; align-items: center; gap: 7px; border: 1px solid rgba(255,255,255,0.14); background: rgba(22,26,40,0.88); color: #e8eaf2; font: 600 12.5px/1 "Segoe UI","PingFang SC","Microsoft YaHei",system-ui,sans-serif; padding: 9px 13px; border-radius: 10px; cursor: pointer; box-shadow: 0 8px 22px rgba(0,0,0,0.35); backdrop-filter: blur(10px); transition: border-color .15s ease, transform .15s ease; }
|
||||
.pna-btn:hover { border-color: rgba(109,141,255,0.65); transform: translateY(-1px); }
|
||||
.pna-btn svg { width: 14px; height: 14px; }
|
||||
#pna-modal { position: fixed; inset: 0; z-index: 2147483001; display: grid; place-items: center; background: rgba(8,10,20,0.55); backdrop-filter: blur(4px); padding: 20px; }
|
||||
#pna-modal[hidden] { display: none; }
|
||||
.pna-card { width: min(360px, 100%); background: rgba(22,26,40,0.94); border: 1px solid rgba(255,255,255,0.12); border-radius: 16px; padding: 24px 22px 18px; box-shadow: 0 24px 60px rgba(0,0,0,0.5); color: #e8eaf2; font: 14px/1.5 "Segoe UI","PingFang SC","Microsoft YaHei",system-ui,sans-serif; }
|
||||
.pna-card h3 { margin: 0 0 14px; font-size: 16px; font-weight: 650; }
|
||||
.pna-card label { display: block; font-size: 12px; color: #9aa1b5; margin: 10px 0 4px; }
|
||||
.pna-card input { width: 100%; box-sizing: border-box; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.14); border-radius: 9px; color: #e8eaf2; padding: 9px 11px; font-size: 14px; outline: none; }
|
||||
.pna-card input:focus { border-color: #6d8dff; box-shadow: 0 0 0 3px rgba(109,141,255,0.2); }
|
||||
.pna-error { background: rgba(248,113,113,0.12); border: 1px solid rgba(248,113,113,0.4); color: #f87171; border-radius: 8px; padding: 7px 10px; font-size: 12.5px; margin-bottom: 4px; }
|
||||
.pna-ok { background: rgba(74,222,128,0.12); border: 1px solid rgba(74,222,128,0.4); color: #4ade80; border-radius: 8px; padding: 7px 10px; font-size: 12.5px; margin-bottom: 4px; }
|
||||
.pna-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
||||
.pna-actions button { border: 1px solid rgba(255,255,255,0.14); background: transparent; color: #e8eaf2; font-size: 13px; padding: 8px 14px; border-radius: 9px; cursor: pointer; }
|
||||
.pna-actions .pna-primary { background: linear-gradient(135deg, #6d8dff, #8b5cf6); border: none; color: #fff; font-weight: 600; }
|
||||
.pna-actions .pna-primary:disabled { opacity: 0.6; cursor: wait; }
|
||||
</style>
|
||||
<div id="pna-widget">
|
||||
<button id="pna-change" class="pna-btn" title="修改面板密码">
|
||||
<svg viewBox="0 0 24 24" fill="none"><rect x="4" y="10" width="16" height="10" rx="2.5" stroke="currentColor" stroke-width="1.8"/><path d="M8 10V7a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="15" r="1.6" fill="currentColor"/></svg>
|
||||
修改密码
|
||||
</button>
|
||||
<button id="pna-logout" class="pna-btn" title="退出登录">
|
||||
<svg viewBox="0 0 24 24" fill="none"><path d="M9 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M15 8l4 4-4 4M19 12H9" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
<div id="pna-modal" hidden>
|
||||
<div class="pna-card">
|
||||
<h3>修改面板密码</h3>
|
||||
<div id="pna-msg" hidden></div>
|
||||
<label for="pna-old">当前密码</label>
|
||||
<input id="pna-old" type="password" autocomplete="current-password">
|
||||
<label for="pna-new">新密码(至少 8 位)</label>
|
||||
<input id="pna-new" type="password" autocomplete="new-password">
|
||||
<label for="pna-confirm">确认新密码</label>
|
||||
<input id="pna-confirm" type="password" autocomplete="new-password">
|
||||
<div class="pna-actions">
|
||||
<button id="pna-cancel" type="button">取消</button>
|
||||
<button id="pna-submit" class="pna-primary" type="button">确认修改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var LOGOUT_URL = ${logoutUrl};
|
||||
var CHANGE_URL = ${changeUrl};
|
||||
var modal = document.getElementById('pna-modal');
|
||||
var msg = document.getElementById('pna-msg');
|
||||
var oldInput = document.getElementById('pna-old');
|
||||
var newInput = document.getElementById('pna-new');
|
||||
var confirmInput = document.getElementById('pna-confirm');
|
||||
var submitBtn = document.getElementById('pna-submit');
|
||||
function showMsg(text, ok) {
|
||||
msg.hidden = false;
|
||||
msg.textContent = text;
|
||||
msg.className = ok ? 'pna-ok' : 'pna-error';
|
||||
}
|
||||
function openModal() {
|
||||
modal.hidden = false;
|
||||
msg.hidden = true;
|
||||
oldInput.value = newInput.value = confirmInput.value = '';
|
||||
oldInput.focus();
|
||||
}
|
||||
function closeModal() {
|
||||
modal.hidden = true;
|
||||
}
|
||||
document.getElementById('pna-logout').addEventListener('click', function () {
|
||||
window.location.href = LOGOUT_URL;
|
||||
});
|
||||
document.getElementById('pna-change').addEventListener('click', openModal);
|
||||
document.getElementById('pna-cancel').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function (event) {
|
||||
if (event.target === modal) closeModal();
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape' && !modal.hidden) closeModal();
|
||||
});
|
||||
submitBtn.addEventListener('click', function () {
|
||||
var oldPassword = oldInput.value;
|
||||
var newPassword = newInput.value;
|
||||
if (!oldPassword || !newPassword) { showMsg('请填写当前密码和新密码', false); return; }
|
||||
if (newPassword.length < 8) { showMsg('新密码至少 8 位', false); return; }
|
||||
if (newPassword !== confirmInput.value) { showMsg('两次输入的新密码不一致', false); return; }
|
||||
submitBtn.disabled = true;
|
||||
fetch(CHANGE_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ oldPassword: oldPassword, newPassword: newPassword }),
|
||||
}).then(function (response) {
|
||||
return response.json().then(function (data) {
|
||||
if (response.ok) {
|
||||
showMsg(data.message || '密码已更新', true);
|
||||
oldInput.value = newInput.value = confirmInput.value = '';
|
||||
setTimeout(closeModal, 1400);
|
||||
} else {
|
||||
showMsg(data.message || '修改失败', false);
|
||||
}
|
||||
}).catch(function () {
|
||||
showMsg(response.ok ? '密码已更新' : '修改失败,请稍后再试', response.ok);
|
||||
});
|
||||
}).catch(function () {
|
||||
showMsg('网络错误,请稍后再试', false);
|
||||
}).finally(function () {
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>`
|
||||
}
|
||||
|
||||
/** Inject the widget before </body>; no-op when the marker is absent. */
|
||||
export function injectWidget(html, widget) {
|
||||
if (typeof html !== 'string') return html
|
||||
const at = html.lastIndexOf('</body>')
|
||||
if (at === -1) return html
|
||||
return html.slice(0, at) + widget + html.slice(at)
|
||||
}
|
||||
|
||||
// ── request helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function jsonResponse(res, status, payload) {
|
||||
@@ -961,8 +830,6 @@ export default function panelAuth(ctx, config) {
|
||||
})
|
||||
const failedLoginDelayMs = positiveInt(cfg.failedLoginDelayMs, 300)
|
||||
console.log('[panel-auth] audit log: ' + auditPath)
|
||||
const widget = renderAuthWidget({ logoutPath, changePasswordPath })
|
||||
ctx.effect(() => ctx.webServer.tapIndex((html) => injectWidget(html, widget)), 'panel-auth: auth widget')
|
||||
let disposer
|
||||
let timer
|
||||
let stopped = false
|
||||
|
||||
Reference in New Issue
Block a user