Merge claude/1
This commit is contained in:
commit
4078468a97
@ -739,6 +739,71 @@
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@ -209,6 +209,15 @@ class BackupPage {
|
||||
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"]');
|
||||
if (locTab) {
|
||||
const tabIdx = locTab.dataset.loc;
|
||||
@ -704,7 +713,7 @@ class BackupPage {
|
||||
<div class="tabs-content">
|
||||
<div class="tab-panel active" data-tab-panel="connection" data-loc="${idx}">
|
||||
<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 class="tab-panel" data-tab-panel="retention" data-loc="${idx}">
|
||||
@ -778,7 +787,7 @@ class BackupPage {
|
||||
|
||||
const conn = document.getElementById(`backup-location-${idx}-connection`);
|
||||
if (conn) {
|
||||
conn.innerHTML = this.renderLocFields(idx, groups.connection, { ...loc, type });
|
||||
conn.innerHTML = this.renderConnectionInner(idx, type, { ...loc, type }, groups.connection);
|
||||
this.tagFieldsForSave(conn);
|
||||
}
|
||||
|
||||
@ -807,10 +816,13 @@ class BackupPage {
|
||||
applySshAuthVisibility(scope) {
|
||||
const authSelect = scope.querySelector('select[name$="_SSH_AUTH"]');
|
||||
if (!authSelect) return;
|
||||
const isPassword = authSelect.value === 'password';
|
||||
const passInput = scope.querySelector('input[name$="_SSH_PASS"]');
|
||||
const passGroup = passInput?.closest('.field-group') || passInput?.closest('.password-mode-wrapper')?.parentElement;
|
||||
if (!passGroup) return;
|
||||
passGroup.style.display = authSelect.value === 'password' ? '' : 'none';
|
||||
if (passGroup) passGroup.style.display = isPassword ? '' : '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. */
|
||||
@ -1520,6 +1532,69 @@ class BackupPage {
|
||||
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) {
|
||||
container.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
|
||||
if (!el.hasAttribute('data-backup-field')) {
|
||||
|
||||
@ -6,6 +6,14 @@
|
||||
# separately. The CFG_BACKUP_LOC_<idx>_SSH_AUTH selector (key|password)
|
||||
# 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()
|
||||
{
|
||||
local idx="$1"
|
||||
@ -54,6 +62,7 @@ backupSshKeySet()
|
||||
fi
|
||||
|
||||
isSuccessful "SSH key saved for location $idx"
|
||||
backupSshKeyRefreshUi
|
||||
}
|
||||
|
||||
backupSshKeyGenerate()
|
||||
@ -81,6 +90,7 @@ backupSshKeyGenerate()
|
||||
isSuccessful "Generated ed25519 keypair for location $idx"
|
||||
isNotice "Public key (paste into the remote host's ~/.ssh/authorized_keys):"
|
||||
backupSshKeyPublic "$idx"
|
||||
backupSshKeyRefreshUi
|
||||
}
|
||||
|
||||
backupSshKeyPublic()
|
||||
@ -99,4 +109,5 @@ backupSshKeyDelete()
|
||||
key_file=$(backupSshKeyFile "$idx")
|
||||
[[ -f "$key_file" ]] && sudo rm -f "$key_file" "${key_file}.pub"
|
||||
isSuccessful "SSH key removed for location $idx"
|
||||
backupSshKeyRefreshUi
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user