refactor(webui): fold SSH Access into an Admin area
Rename the Config top-nav to 'Admin' and move SSH Access into its sidebar
under a 'Tools' group, instead of a separate top-level nav item. SSH Access is
rendered by SshPage into the config main pane via a renderConfig('ssh-access')
special case; the sidebar item (config-sidebar.js) routes there. SshPage now
mounts into any container (defaults to #config-section). /ssh redirects to
/config?=ssh-access for old links; the standalone ssh-content.html is removed.
Declutters the top bar and gives system/admin features one home that scales
(updates, users, Connect settings can become sidebar entries later).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
403b7055c8
commit
4fd043a852
@ -4,7 +4,19 @@
|
|||||||
.ssh-page {
|
.ssh-page {
|
||||||
max-width: 860px;
|
max-width: 860px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 20px 40px;
|
padding: 8px 4px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Tools" group heading in the Admin (config) sidebar, above SSH Access. */
|
||||||
|
.sidebar-group-label {
|
||||||
|
margin: 14px 8px 4px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid rgba(var(--text-rgb), 0.08);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(var(--text-rgb), 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssh-page-header {
|
.ssh-page-header {
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
<div class="container ssh-page">
|
|
||||||
<div class="ssh-page-header">
|
|
||||||
<h1>SSH Access</h1>
|
|
||||||
<p class="ssh-page-sub">Control who can SSH into this server. Grant access by adding a public key, and optionally require key-only login.</p>
|
|
||||||
</div>
|
|
||||||
<div id="ssh-page-root">
|
|
||||||
<div class="backup-empty-state">Loading…</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
Config
|
Admin
|
||||||
</a>
|
</a>
|
||||||
<a href="tasks.html" class="nav-item" id="nav-tasks">
|
<a href="tasks.html" class="nav-item" id="nav-tasks">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@ -51,15 +51,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Backups
|
Backups
|
||||||
</a>
|
</a>
|
||||||
<a href="/ssh" class="nav-item" id="nav-ssh">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="7.5" cy="15.5" r="4.5"></circle>
|
|
||||||
<path d="M10.7 12.3 19 4"></path>
|
|
||||||
<path d="M17 6l2 2"></path>
|
|
||||||
<path d="M15 8l2 2"></path>
|
|
||||||
</svg>
|
|
||||||
SSH Access
|
|
||||||
</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div class="mobile-drawer-page-section" id="mobile-drawer-page-section"></div>
|
<div class="mobile-drawer-page-section" id="mobile-drawer-page-section"></div>
|
||||||
<div class="topbar-controls">
|
<div class="topbar-controls">
|
||||||
|
|||||||
@ -25,6 +25,19 @@ if (typeof window.ConfigManager === 'undefined') {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSH Access is an admin tool page that lives in this sidebar rather than
|
||||||
|
// a config category — render its own controller into the main pane.
|
||||||
|
if (category === 'ssh-access') {
|
||||||
|
try { this.sidebar.populateSidebar(); } catch (e) {}
|
||||||
|
if (typeof SshPage !== 'undefined') {
|
||||||
|
window.sshPage = new SshPage('config-section');
|
||||||
|
await window.sshPage.init();
|
||||||
|
} else {
|
||||||
|
configSection.innerHTML = '<div class="error">SSH Access page failed to load.</div>';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Show loading state with enhanced box styling
|
// Show loading state with enhanced box styling
|
||||||
configSection.innerHTML = `
|
configSection.innerHTML = `
|
||||||
|
|||||||
@ -73,7 +73,29 @@ class ConfigSidebar {
|
|||||||
|
|
||||||
self.categoriesList.appendChild(categoryItem);
|
self.categoriesList.appendChild(categoryItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tools group — admin pages that live in this area but aren't config
|
||||||
|
// categories (rendered by their own controller, not the config form).
|
||||||
|
const toolsLabel = document.createElement('div');
|
||||||
|
toolsLabel.className = 'sidebar-group-label';
|
||||||
|
toolsLabel.textContent = 'Tools';
|
||||||
|
self.categoriesList.appendChild(toolsLabel);
|
||||||
|
|
||||||
|
const sshItem = document.createElement('div');
|
||||||
|
sshItem.className = 'category';
|
||||||
|
sshItem.setAttribute('data-category', 'ssh-access');
|
||||||
|
sshItem.innerHTML = '<img src="/icons/config/security.svg" alt="SSH Access" style="width: 20px; height: 20px; margin-right: 8px;"/> SSH Access';
|
||||||
|
sshItem.addEventListener('click', function () {
|
||||||
|
window.history.pushState({}, '', '/config?=ssh-access');
|
||||||
|
document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); });
|
||||||
|
this.classList.add('active');
|
||||||
|
window.configCategory = 'ssh-access';
|
||||||
|
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
|
||||||
|
window.configManager.renderConfig('ssh-access');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.categoriesList.appendChild(sshItem);
|
||||||
|
|
||||||
// Set initial active category
|
// Set initial active category
|
||||||
this.setActiveCategory(window.configCategory || 'general');
|
this.setActiveCategory(window.configCategory || 'general');
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
// SSH Access page — manage inbound admin SSH to this host: authorize public
|
// SSH Access — inbound admin SSH to this host. Lives in the Admin area (a
|
||||||
// keys (paste to grant access), remove them, and toggle password login behind
|
// sidebar item on the Config/Admin page) and renders into whatever container
|
||||||
// the backend's lockout guard. Reads /data/ssh/access.json; all mutations run
|
// it's given (defaults to #config-section). Authorize public keys (paste to
|
||||||
// as `libreportal ssh ...` tasks. LibrePortal never handles a private key here.
|
// grant access), remove them, and toggle password login behind the backend's
|
||||||
|
// lockout guard. Reads /data/ssh/access.json; mutations run as
|
||||||
|
// `libreportal ssh ...` tasks. LibrePortal never handles a private key here.
|
||||||
class SshPage {
|
class SshPage {
|
||||||
constructor() {
|
constructor(rootId = 'config-section') {
|
||||||
|
this.rootId = rootId;
|
||||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||||
this.data = null;
|
this.data = null;
|
||||||
this._bound = false;
|
this._bound = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
root() { return document.getElementById(this.rootId); }
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
const r = this.root();
|
||||||
|
if (r) r.innerHTML = '<div class="ssh-page"><div class="backup-empty-state">Loading…</div></div>';
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
this.render();
|
this.render();
|
||||||
@ -48,10 +55,10 @@ class SshPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const root = document.getElementById('ssh-page-root');
|
const root = this.root();
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
if (!this.data) {
|
if (!this.data) {
|
||||||
root.innerHTML = `<div class="backup-empty-state">Couldn't load SSH access data.</div>`;
|
root.innerHTML = `<div class="ssh-page"><div class="backup-empty-state">Couldn't load SSH access data.</div></div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = this.data;
|
const d = this.data;
|
||||||
@ -69,32 +76,39 @@ class SshPage {
|
|||||||
</div>`).join('') : `<div class="backup-empty-state">No keys authorized yet — add one below to allow key-based login.</div>`;
|
</div>`).join('') : `<div class="backup-empty-state">No keys authorized yet — add one below to allow key-based login.</div>`;
|
||||||
|
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<div class="backup-ssh-key-card">
|
<div class="ssh-page">
|
||||||
<div class="backup-ssh-key-head">
|
<div class="ssh-page-header">
|
||||||
<span class="backup-ssh-key-title">Login</span>
|
<h1>SSH Access</h1>
|
||||||
<span class="backup-ssh-key-status ${pwOn ? 'none' : 'ok'}">${pwOn ? 'Password login: ON' : 'Key-only login'}</span>
|
<p class="ssh-page-sub">Control who can SSH into this server. Grant access by adding a public key, and optionally require key-only login.</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="backup-card-hint">Logging in as <code>${this.escape(d.user)}</code> · ${keys.length} authorized key${keys.length === 1 ? '' : 's'}.</p>
|
|
||||||
<div class="backup-ssh-key-actions">
|
|
||||||
${pwOn
|
|
||||||
? `<button type="button" class="backup-secondary-btn" data-action="ssh-toggle-password" data-next="off">Require key-only login</button>`
|
|
||||||
: `<button type="button" class="backup-secondary-btn" data-action="ssh-toggle-password" data-next="on">Re-enable password login</button>`}
|
|
||||||
</div>
|
|
||||||
${pwOn ? '' : `<p class="backup-card-hint" style="margin-top:8px">Password login is disabled — only the keys below can connect.</p>`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="backup-ssh-key-card">
|
<div class="backup-ssh-key-card">
|
||||||
<div class="backup-ssh-key-head"><span class="backup-ssh-key-title">Add a key</span></div>
|
<div class="backup-ssh-key-head">
|
||||||
<p class="backup-card-hint">Paste a <strong>public</strong> key (the <code>.pub</code> from your machine) to grant it SSH access:</p>
|
<span class="backup-ssh-key-title">Login</span>
|
||||||
<textarea class="backup-ssh-keyinput" id="ssh-add-key-input" rows="3" spellcheck="false" placeholder="ssh-ed25519 AAAA... you@laptop"></textarea>
|
<span class="backup-ssh-key-status ${pwOn ? 'none' : 'ok'}">${pwOn ? 'Password login: ON' : 'Key-only login'}</span>
|
||||||
<div class="backup-ssh-key-actions">
|
</div>
|
||||||
<button type="button" class="backup-primary-btn" data-action="ssh-add-key">Authorize key</button>
|
<p class="backup-card-hint">Logging in as <code>${this.escape(d.user)}</code> · ${keys.length} authorized key${keys.length === 1 ? '' : 's'}.</p>
|
||||||
|
<div class="backup-ssh-key-actions">
|
||||||
|
${pwOn
|
||||||
|
? `<button type="button" class="backup-secondary-btn" data-action="ssh-toggle-password" data-next="off">Require key-only login</button>`
|
||||||
|
: `<button type="button" class="backup-secondary-btn" data-action="ssh-toggle-password" data-next="on">Re-enable password login</button>`}
|
||||||
|
</div>
|
||||||
|
${pwOn ? '' : `<p class="backup-card-hint" style="margin-top:8px">Password login is disabled — only the keys below can connect.</p>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="backup-ssh-key-card">
|
<div class="backup-ssh-key-card">
|
||||||
<div class="backup-ssh-key-head"><span class="backup-ssh-key-title">Authorized keys</span></div>
|
<div class="backup-ssh-key-head"><span class="backup-ssh-key-title">Add a key</span></div>
|
||||||
<div class="ssh-key-list">${keysHtml}</div>
|
<p class="backup-card-hint">Paste a <strong>public</strong> key (the <code>.pub</code> from your machine) to grant it SSH access:</p>
|
||||||
|
<textarea class="backup-ssh-keyinput" id="ssh-add-key-input" rows="3" spellcheck="false" placeholder="ssh-ed25519 AAAA... you@laptop"></textarea>
|
||||||
|
<div class="backup-ssh-key-actions">
|
||||||
|
<button type="button" class="backup-primary-btn" data-action="ssh-add-key">Authorize key</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="backup-ssh-key-card">
|
||||||
|
<div class="backup-ssh-key-head"><span class="backup-ssh-key-title">Authorized keys</span></div>
|
||||||
|
<div class="ssh-key-list">${keysHtml}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -110,8 +124,7 @@ class SshPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addKey() {
|
async addKey() {
|
||||||
const input = document.getElementById('ssh-add-key-input');
|
const key = (document.getElementById('ssh-add-key-input')?.value || '').trim();
|
||||||
const key = (input?.value || '').trim();
|
|
||||||
if (!key) { this.notify('Paste a public key first', 'error'); return; }
|
if (!key) { this.notify('Paste a public key first', 'error'); return; }
|
||||||
const b64 = btoa(unescape(encodeURIComponent(key)));
|
const b64 = btoa(unescape(encodeURIComponent(key)));
|
||||||
await this.runTask(`libreportal ssh key-add ${b64}`);
|
await this.runTask(`libreportal ssh key-add ${b64}`);
|
||||||
|
|||||||
@ -273,7 +273,7 @@ class TopbarComponent {
|
|||||||
} else if (path.startsWith('/backup')) {
|
} else if (path.startsWith('/backup')) {
|
||||||
activeNavId = 'nav-backup';
|
activeNavId = 'nav-backup';
|
||||||
} else if (path.startsWith('/ssh')) {
|
} else if (path.startsWith('/ssh')) {
|
||||||
activeNavId = 'nav-ssh';
|
activeNavId = 'nav-config'; // SSH Access lives under the Admin (config) area
|
||||||
} else if (path === '/' || path === '/dashboard') {
|
} else if (path === '/' || path === '/dashboard') {
|
||||||
activeNavId = 'nav-dashboard';
|
activeNavId = 'nav-dashboard';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -270,19 +270,9 @@ class LibrePortalSPAClean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSsh() {
|
async handleSsh() {
|
||||||
try {
|
// SSH Access now lives inside the Admin (config) area as a sidebar item.
|
||||||
const html = await this.fetchContent('/html/ssh-content.html');
|
// Redirect old /ssh links to it.
|
||||||
this.loadContent(html, 'SSH Access');
|
this.navigate('/config?=ssh-access', true);
|
||||||
if (typeof SshPage !== 'undefined') {
|
|
||||||
window.sshPage = new SshPage();
|
|
||||||
await window.sshPage.init();
|
|
||||||
} else {
|
|
||||||
console.error('SshPage class not loaded');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ SSH page load error:', error);
|
|
||||||
this.showError('Failed to load SSH page');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleApps() {
|
async handleApps() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user