Compare commits
2 Commits
5db4d518cd
...
4078468a97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4078468a97 | ||
|
|
d3faa2514f |
@ -739,6 +739,71 @@
|
|||||||
padding: 20px 22px;
|
padding: 20px 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SSH key card (sftp locations). LibrePortal holds the private key; only the
|
||||||
|
public key is shown — that's what goes in the remote's authorized_keys. */
|
||||||
|
.backup-ssh-key-card {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid rgba(var(--text-rgb), 0.10);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(var(--text-rgb), 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-key-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-key-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-key-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-key-status.ok {
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(var(--accent-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-key-status.none {
|
||||||
|
color: rgba(var(--text-rgb), 0.6);
|
||||||
|
background: rgba(var(--text-rgb), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-pubkey,
|
||||||
|
.backup-ssh-keyinput {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid rgba(var(--text-rgb), 0.12);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-pubkey {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-ssh-key-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.backup-form-footer {
|
.backup-form-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@ -209,6 +209,15 @@ class BackupPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sshSave = e.target.closest('[data-action="ssh-key-save"]');
|
||||||
|
if (sshSave) { this.saveBackupSshKey(parseInt(sshSave.dataset.loc, 10)); return; }
|
||||||
|
const sshGen = e.target.closest('[data-action="ssh-key-generate"]');
|
||||||
|
if (sshGen) { this.generateBackupSshKey(parseInt(sshGen.dataset.loc, 10)); return; }
|
||||||
|
const sshDel = e.target.closest('[data-action="ssh-key-delete"]');
|
||||||
|
if (sshDel) { this.deleteBackupSshKey(parseInt(sshDel.dataset.loc, 10)); return; }
|
||||||
|
const sshCopy = e.target.closest('[data-action="ssh-key-copy"]');
|
||||||
|
if (sshCopy) { this.copyBackupSshKey(parseInt(sshCopy.dataset.loc, 10)); return; }
|
||||||
|
|
||||||
const locTab = e.target.closest('[data-action="loc-tab"]');
|
const locTab = e.target.closest('[data-action="loc-tab"]');
|
||||||
if (locTab) {
|
if (locTab) {
|
||||||
const tabIdx = locTab.dataset.loc;
|
const tabIdx = locTab.dataset.loc;
|
||||||
@ -704,7 +713,7 @@ class BackupPage {
|
|||||||
<div class="tabs-content">
|
<div class="tabs-content">
|
||||||
<div class="tab-panel active" data-tab-panel="connection" data-loc="${idx}">
|
<div class="tab-panel active" data-tab-panel="connection" data-loc="${idx}">
|
||||||
<div class="backup-location-connection-fields" id="backup-location-${idx}-connection">
|
<div class="backup-location-connection-fields" id="backup-location-${idx}-connection">
|
||||||
${this.renderLocFields(idx, groups.connection, l)}
|
${this.renderConnectionInner(idx, l.type, l, groups.connection)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-panel" data-tab-panel="retention" data-loc="${idx}">
|
<div class="tab-panel" data-tab-panel="retention" data-loc="${idx}">
|
||||||
@ -778,7 +787,7 @@ class BackupPage {
|
|||||||
|
|
||||||
const conn = document.getElementById(`backup-location-${idx}-connection`);
|
const conn = document.getElementById(`backup-location-${idx}-connection`);
|
||||||
if (conn) {
|
if (conn) {
|
||||||
conn.innerHTML = this.renderLocFields(idx, groups.connection, { ...loc, type });
|
conn.innerHTML = this.renderConnectionInner(idx, type, { ...loc, type }, groups.connection);
|
||||||
this.tagFieldsForSave(conn);
|
this.tagFieldsForSave(conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -807,10 +816,13 @@ class BackupPage {
|
|||||||
applySshAuthVisibility(scope) {
|
applySshAuthVisibility(scope) {
|
||||||
const authSelect = scope.querySelector('select[name$="_SSH_AUTH"]');
|
const authSelect = scope.querySelector('select[name$="_SSH_AUTH"]');
|
||||||
if (!authSelect) return;
|
if (!authSelect) return;
|
||||||
|
const isPassword = authSelect.value === 'password';
|
||||||
const passInput = scope.querySelector('input[name$="_SSH_PASS"]');
|
const passInput = scope.querySelector('input[name$="_SSH_PASS"]');
|
||||||
const passGroup = passInput?.closest('.field-group') || passInput?.closest('.password-mode-wrapper')?.parentElement;
|
const passGroup = passInput?.closest('.field-group') || passInput?.closest('.password-mode-wrapper')?.parentElement;
|
||||||
if (!passGroup) return;
|
if (passGroup) passGroup.style.display = isPassword ? '' : 'none';
|
||||||
passGroup.style.display = authSelect.value === 'password' ? '' : 'none';
|
// SSH key card is the counterpart: shown for key auth, hidden for password.
|
||||||
|
const keyCard = scope.querySelector('.backup-ssh-key-card');
|
||||||
|
if (keyCard) keyCard.style.display = isPassword ? 'none' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the custom PATH input when PATH_MODE=auto, show when =custom. */
|
/* Hide the custom PATH input when PATH_MODE=auto, show when =custom. */
|
||||||
@ -1520,6 +1532,69 @@ class BackupPage {
|
|||||||
return { connection, advanced };
|
return { connection, advanced };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Connection-tab body: the generic fields plus, for sftp, the SSH key card.
|
||||||
|
Used both on first render and when the Type select changes. */
|
||||||
|
renderConnectionInner(idx, type, loc, connectionSuffixes) {
|
||||||
|
let html = this.renderLocFields(idx, connectionSuffixes, loc);
|
||||||
|
if (type === 'sftp') html += this.renderBackupSshKeyCard(loc);
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SSH key card for an sftp location. LibrePortal holds the private key in
|
||||||
|
the location's ssh.key file; only the public key is shown — that's what
|
||||||
|
you paste into the remote server's authorized_keys. Hidden by
|
||||||
|
applySshAuthVisibility when SSH auth = password. */
|
||||||
|
renderBackupSshKeyCard(l) {
|
||||||
|
const idx = l.idx;
|
||||||
|
const hasKey = l.ssh_key_exists === true;
|
||||||
|
const pub = l.ssh_public_key || '';
|
||||||
|
const body = hasKey ? `
|
||||||
|
<p class="backup-card-hint">Add this public key to the remote server's <code>~/.ssh/authorized_keys</code>:</p>
|
||||||
|
<textarea class="backup-ssh-pubkey" readonly rows="2" spellcheck="false">${this.escape(pub)}</textarea>
|
||||||
|
<div class="backup-ssh-key-actions">
|
||||||
|
<button type="button" class="backup-secondary-btn" data-action="ssh-key-copy" data-loc="${idx}">Copy public key</button>
|
||||||
|
<button type="button" class="backup-danger-btn" data-action="ssh-key-delete" data-loc="${idx}">Delete key</button>
|
||||||
|
</div>` : `
|
||||||
|
<p class="backup-card-hint">Paste an existing private key, or generate one and we'll show the public key to add on the remote.</p>
|
||||||
|
<textarea class="backup-ssh-keyinput" rows="4" spellcheck="false" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"></textarea>
|
||||||
|
<div class="backup-ssh-key-actions">
|
||||||
|
<button type="button" class="backup-primary-btn" data-action="ssh-key-save" data-loc="${idx}">Save key</button>
|
||||||
|
<button type="button" class="backup-secondary-btn" data-action="ssh-key-generate" data-loc="${idx}">Generate keypair</button>
|
||||||
|
</div>`;
|
||||||
|
return `
|
||||||
|
<div class="backup-ssh-key-card" data-loc="${idx}">
|
||||||
|
<div class="backup-ssh-key-head">
|
||||||
|
<span class="backup-ssh-key-title">SSH key</span>
|
||||||
|
<span class="backup-ssh-key-status ${hasKey ? 'ok' : 'none'}">${hasKey ? '✓ Key configured' : 'No key yet'}</span>
|
||||||
|
</div>
|
||||||
|
${body}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveBackupSshKey(idx) {
|
||||||
|
const card = document.querySelector(`.backup-ssh-key-card[data-loc="${idx}"]`);
|
||||||
|
const key = (card?.querySelector('.backup-ssh-keyinput')?.value || '').trim();
|
||||||
|
if (!key) { this.notify('Paste a private key first', 'error'); return; }
|
||||||
|
const b64 = btoa(unescape(encodeURIComponent(key + '\n')));
|
||||||
|
await this.runTask(`libreportal backup location ssh-key-set ${idx} ${b64}`, 'backup', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateBackupSshKey(idx) {
|
||||||
|
await this.runTask(`libreportal backup location ssh-key-generate ${idx}`, 'backup', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBackupSshKey(idx) {
|
||||||
|
if (!confirm("Delete this location's SSH key? Backups here will fail until a new key is set and added on the remote.")) return;
|
||||||
|
await this.runTask(`libreportal backup location ssh-key-delete ${idx}`, 'backup', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyBackupSshKey(idx) {
|
||||||
|
const loc = (this.locations?.locations || []).find(l => l.idx === idx);
|
||||||
|
const pub = loc?.ssh_public_key || '';
|
||||||
|
try { await navigator.clipboard.writeText(pub); this.notify('Public key copied', 'success'); }
|
||||||
|
catch { this.notify('Copy failed — select the text and copy manually', 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
tagFieldsForSave(container) {
|
tagFieldsForSave(container) {
|
||||||
container.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
|
container.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
|
||||||
if (!el.hasAttribute('data-backup-field')) {
|
if (!el.hasAttribute('data-backup-field')) {
|
||||||
|
|||||||
@ -6,6 +6,14 @@
|
|||||||
# separately. The CFG_BACKUP_LOC_<idx>_SSH_AUTH selector (key|password)
|
# separately. The CFG_BACKUP_LOC_<idx>_SSH_AUTH selector (key|password)
|
||||||
# is the only ssh-related value in the location.config file.
|
# is the only ssh-related value in the location.config file.
|
||||||
|
|
||||||
|
# Refresh locations.json so the editor reflects new key state (public key /
|
||||||
|
# "key set" status) right after a mutation. No-op if the generator isn't loaded.
|
||||||
|
backupSshKeyRefreshUi()
|
||||||
|
{
|
||||||
|
declare -f webuiGenerateBackupLocations >/dev/null 2>&1 && webuiGenerateBackupLocations >/dev/null 2>&1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
backupSshKeyFile()
|
backupSshKeyFile()
|
||||||
{
|
{
|
||||||
local idx="$1"
|
local idx="$1"
|
||||||
@ -54,6 +62,7 @@ backupSshKeySet()
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
isSuccessful "SSH key saved for location $idx"
|
isSuccessful "SSH key saved for location $idx"
|
||||||
|
backupSshKeyRefreshUi
|
||||||
}
|
}
|
||||||
|
|
||||||
backupSshKeyGenerate()
|
backupSshKeyGenerate()
|
||||||
@ -81,6 +90,7 @@ backupSshKeyGenerate()
|
|||||||
isSuccessful "Generated ed25519 keypair for location $idx"
|
isSuccessful "Generated ed25519 keypair for location $idx"
|
||||||
isNotice "Public key (paste into the remote host's ~/.ssh/authorized_keys):"
|
isNotice "Public key (paste into the remote host's ~/.ssh/authorized_keys):"
|
||||||
backupSshKeyPublic "$idx"
|
backupSshKeyPublic "$idx"
|
||||||
|
backupSshKeyRefreshUi
|
||||||
}
|
}
|
||||||
|
|
||||||
backupSshKeyPublic()
|
backupSshKeyPublic()
|
||||||
@ -99,4 +109,5 @@ backupSshKeyDelete()
|
|||||||
key_file=$(backupSshKeyFile "$idx")
|
key_file=$(backupSshKeyFile "$idx")
|
||||||
[[ -f "$key_file" ]] && sudo rm -f "$key_file" "${key_file}.pub"
|
[[ -f "$key_file" ]] && sudo rm -f "$key_file" "${key_file}.pub"
|
||||||
isSuccessful "SSH key removed for location $idx"
|
isSuccessful "SSH key removed for location $idx"
|
||||||
|
backupSshKeyRefreshUi
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user