diff --git a/containers/libreportal/frontend/css/ssh.css b/containers/libreportal/frontend/css/ssh.css index 078f268..0b55c7d 100644 --- a/containers/libreportal/frontend/css/ssh.css +++ b/containers/libreportal/frontend/css/ssh.css @@ -4,7 +4,19 @@ .ssh-page { max-width: 860px; 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 { diff --git a/containers/libreportal/frontend/html/ssh-content.html b/containers/libreportal/frontend/html/ssh-content.html deleted file mode 100644 index 1ba6ee1..0000000 --- a/containers/libreportal/frontend/html/ssh-content.html +++ /dev/null @@ -1,9 +0,0 @@ -
-
-

SSH Access

-

Control who can SSH into this server. Grant access by adding a public key, and optionally require key-only login.

-
-
-
Loading…
-
-
diff --git a/containers/libreportal/frontend/html/topbar.html b/containers/libreportal/frontend/html/topbar.html index 2bd09e2..e14d7a2 100755 --- a/containers/libreportal/frontend/html/topbar.html +++ b/containers/libreportal/frontend/html/topbar.html @@ -35,7 +35,7 @@ - Config + Admin @@ -51,15 +51,6 @@ Backups - - - - - - - - SSH Access -
diff --git a/containers/libreportal/frontend/js/components/config/config-manager.js b/containers/libreportal/frontend/js/components/config/config-manager.js index 260042d..ae56e75 100755 --- a/containers/libreportal/frontend/js/components/config/config-manager.js +++ b/containers/libreportal/frontend/js/components/config/config-manager.js @@ -25,6 +25,19 @@ if (typeof window.ConfigManager === 'undefined') { 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 = '
SSH Access page failed to load.
'; + } + return; + } + try { // Show loading state with enhanced box styling configSection.innerHTML = ` diff --git a/containers/libreportal/frontend/js/components/config/config-sidebar.js b/containers/libreportal/frontend/js/components/config/config-sidebar.js index 9da7d3c..86ce36a 100755 --- a/containers/libreportal/frontend/js/components/config/config-sidebar.js +++ b/containers/libreportal/frontend/js/components/config/config-sidebar.js @@ -73,7 +73,29 @@ class ConfigSidebar { 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 = 'SSH Access 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 this.setActiveCategory(window.configCategory || 'general'); diff --git a/containers/libreportal/frontend/js/components/ssh/ssh-page.js b/containers/libreportal/frontend/js/components/ssh/ssh-page.js index 3f0afe4..735ff32 100644 --- a/containers/libreportal/frontend/js/components/ssh/ssh-page.js +++ b/containers/libreportal/frontend/js/components/ssh/ssh-page.js @@ -1,15 +1,22 @@ -// SSH Access page — manage inbound admin SSH to this host: authorize public -// keys (paste to grant access), remove them, and toggle password login behind -// the backend's lockout guard. Reads /data/ssh/access.json; all mutations run -// as `libreportal ssh ...` tasks. LibrePortal never handles a private key here. +// SSH Access — inbound admin SSH to this host. Lives in the Admin area (a +// sidebar item on the Config/Admin page) and renders into whatever container +// it's given (defaults to #config-section). Authorize public keys (paste to +// 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 { - constructor() { + constructor(rootId = 'config-section') { + this.rootId = rootId; this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null; this.data = null; this._bound = false; } + root() { return document.getElementById(this.rootId); } + async init() { + const r = this.root(); + if (r) r.innerHTML = '
Loading…
'; this.bindEvents(); await this.refresh(); this.render(); @@ -48,10 +55,10 @@ class SshPage { } render() { - const root = document.getElementById('ssh-page-root'); + const root = this.root(); if (!root) return; if (!this.data) { - root.innerHTML = `
Couldn't load SSH access data.
`; + root.innerHTML = `
Couldn't load SSH access data.
`; return; } const d = this.data; @@ -69,32 +76,39 @@ class SshPage {
`).join('') : `
No keys authorized yet — add one below to allow key-based login.
`; root.innerHTML = ` -
-
- Login - ${pwOn ? 'Password login: ON' : 'Key-only login'} +
+
+

SSH Access

+

Control who can SSH into this server. Grant access by adding a public key, and optionally require key-only login.

-

Logging in as ${this.escape(d.user)} · ${keys.length} authorized key${keys.length === 1 ? '' : 's'}.

-
- ${pwOn - ? `` - : ``} -
- ${pwOn ? '' : `

Password login is disabled — only the keys below can connect.

`} -
-
-
Add a key
-

Paste a public key (the .pub from your machine) to grant it SSH access:

- -
- +
+
+ Login + ${pwOn ? 'Password login: ON' : 'Key-only login'} +
+

Logging in as ${this.escape(d.user)} · ${keys.length} authorized key${keys.length === 1 ? '' : 's'}.

+
+ ${pwOn + ? `` + : ``} +
+ ${pwOn ? '' : `

Password login is disabled — only the keys below can connect.

`}
-
-
-
Authorized keys
-
${keysHtml}
+
+
Add a key
+

Paste a public key (the .pub from your machine) to grant it SSH access:

+ +
+ +
+
+ +
+
Authorized keys
+
${keysHtml}
+
`; } @@ -110,8 +124,7 @@ class SshPage { } async addKey() { - const input = document.getElementById('ssh-add-key-input'); - const key = (input?.value || '').trim(); + const key = (document.getElementById('ssh-add-key-input')?.value || '').trim(); if (!key) { this.notify('Paste a public key first', 'error'); return; } const b64 = btoa(unescape(encodeURIComponent(key))); await this.runTask(`libreportal ssh key-add ${b64}`); diff --git a/containers/libreportal/frontend/js/components/topbar.js b/containers/libreportal/frontend/js/components/topbar.js index 05092e4..082c843 100755 --- a/containers/libreportal/frontend/js/components/topbar.js +++ b/containers/libreportal/frontend/js/components/topbar.js @@ -273,7 +273,7 @@ class TopbarComponent { } else if (path.startsWith('/backup')) { activeNavId = 'nav-backup'; } else if (path.startsWith('/ssh')) { - activeNavId = 'nav-ssh'; + activeNavId = 'nav-config'; // SSH Access lives under the Admin (config) area } else if (path === '/' || path === '/dashboard') { activeNavId = 'nav-dashboard'; } else { diff --git a/containers/libreportal/frontend/js/spa.js b/containers/libreportal/frontend/js/spa.js index b77813c..907cec2 100755 --- a/containers/libreportal/frontend/js/spa.js +++ b/containers/libreportal/frontend/js/spa.js @@ -270,19 +270,9 @@ class LibrePortalSPAClean { } async handleSsh() { - try { - const html = await this.fetchContent('/html/ssh-content.html'); - this.loadContent(html, 'SSH Access'); - 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'); - } + // SSH Access now lives inside the Admin (config) area as a sidebar item. + // Redirect old /ssh links to it. + this.navigate('/config?=ssh-access', true); } async handleApps() {