// 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(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(); } async refresh() { this.data = await this.fetchJson(`/data/ssh/access.json?t=${Date.now()}`); } async fetchJson(url) { try { const r = await fetch(url); if (!r.ok) return null; return await r.json(); } catch { return null; } } escape(s) { return String(s ?? '').replace(/[&<>"']/g, c => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); } notify(msg, type) { if (window.notificationSystem) window.notificationSystem.show(msg, type || 'info'); else console.log(`[ssh ${type || 'info'}] ${msg}`); } bindEvents() { if (this._bound) return; this._bound = true; document.addEventListener('click', (e) => { if (e.target.closest('[data-action="ssh-add-key"]')) { this.addKey(); return; } const rm = e.target.closest('[data-action="ssh-remove-key"]'); if (rm) { this.removeKey(rm.dataset.fp); return; } const tog = e.target.closest('[data-action="ssh-toggle-password"]'); if (tog) { this.togglePassword(tog.dataset.next); return; } }); } render() { const root = this.root(); if (!root) return; if (!this.data) { root.innerHTML = `
Couldn't load SSH access data.
`; return; } const d = this.data; const pwOn = d.password_auth === true; const keys = Array.isArray(d.keys) ? d.keys : []; const keysHtml = keys.length ? keys.map(k => `
${this.escape(k.type)} ${this.escape(k.comment || '(no comment)')} ${this.escape(k.fingerprint)}
`).join('') : `
No keys authorized yet — add one below to allow key-based login.
`; // Reuse the config page's section layout (.config-category/.domains-*) // so SSH Access looks like the rest of the Admin config pages. const section = (title, desc, inner) => `

${title}

${desc}

${inner}
`; const loginInner = `
Password login${pwOn ? 'On' : 'Key-only'}
Authorized keys${keys.length}
${pwOn ? `` : ``}
`; const addInner = `
`; root.innerHTML = `
${section('Login', pwOn ? 'Password login is on. Add a key, then you can switch to key-only.' : 'Password login is disabled — only the keys below can connect.', loginInner)} ${section('Add a key', 'Authorize a new machine to log in.', addInner)}
${section('Authorized keys', 'Machines currently allowed to SSH in.', `
${keysHtml}
`)}
`; } async runTask(command) { if (!this.taskManager) { this.notify('Task system unavailable', 'error'); return; } try { await this.taskManager.createTask(command, 'ssh', null); setTimeout(() => this.refresh().then(() => this.render()), 1500); } catch (e) { this.notify(`Failed to queue task: ${e.message || e}`, 'error'); } } async addKey() { 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}`); } async removeKey(fp) { if (!fp) return; if (!confirm('Remove this key? That machine will no longer be able to SSH in.')) return; await this.runTask(`libreportal ssh key-remove ${fp}`); } async togglePassword(next) { if (next === 'off' && !confirm('Disable password login? Make sure a key above works first — otherwise you could lock yourself out.')) return; await this.runTask(`libreportal ssh password-auth ${next}`); } } window.SshPage = SshPage;