Backup step: gate Next instead of instructing
"Repository password" is restic's word for it, not the user's. It is the Backup Password. And the card no longer ends with "Enter its password below to see what is inside." That line told someone what a locked door is for while they stood in front of it, and kept telling them after they had entered one. The step enforces it instead: a restore cannot advance without a password AND a successful read, because the next step renders what is IN the backup and that means it has to have been opened. The message appears when the user tries to move on, rather than in advance and forever. Editing the path, the type or the password after a read now invalidates it. Without that the wizard would carry the previous repository's contents forward under a new path — a wrong answer that looks entirely plausible, since the Contents step would render perfectly well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
853e51baac
commit
f77915de3b
@ -452,8 +452,8 @@ class SetupWizard {
|
||||
|
||||
<div class="setup-field">
|
||||
<label for="sw-rs-pass">
|
||||
Repository password
|
||||
<span class="setup-tooltip" tabindex="0" data-tip="The password this repository was encrypted with. It is handed to the host through a one-shot reference, so it never lands in a task or a log.">?</span>
|
||||
Backup Password
|
||||
<span class="setup-tooltip" tabindex="0" data-tip="The password this backup was encrypted with. It is handed to the host through a one-shot reference, so it never lands in a task or a log.">?</span>
|
||||
</label>
|
||||
<div class="setup-input-row">
|
||||
<span class="setup-field-icon setup-field-icon-emoji" aria-hidden="true">\u{1F512}</span>
|
||||
@ -1232,8 +1232,10 @@ class SetupWizard {
|
||||
const box = this.container.querySelector('#sw-rs-verify-result');
|
||||
if (!box) return;
|
||||
if (d && d.repo) {
|
||||
box.innerHTML = this._backupCard(d, 'chosen') +
|
||||
`<p class="setup-section-hint">Enter its password below to see what is inside.</p>`;
|
||||
// No "now enter the password" line: the step will not advance without
|
||||
// one, and saying it as well is telling someone what a locked door is
|
||||
// for while they are standing in front of it.
|
||||
box.innerHTML = this._backupCard(d, 'chosen');
|
||||
return;
|
||||
}
|
||||
// A suggestion means they pointed at the folder holding the repositories
|
||||
@ -1367,6 +1369,22 @@ class SetupWizard {
|
||||
if (sel) sel.addEventListener('change', sync);
|
||||
const verify = this.container.querySelector('#sw-rs-verify');
|
||||
if (verify) verify.addEventListener('click', () => this.verifyBackupPath());
|
||||
|
||||
// Changing where the backup is, or its password, invalidates whatever was
|
||||
// read from the last one. Without this the wizard would happily carry the
|
||||
// old repository's contents forward under the new path.
|
||||
const stale = () => {
|
||||
if (!this.restoreInfo) return;
|
||||
this.restoreInfo = null;
|
||||
const st = this.container.querySelector('#sw-rs-status');
|
||||
if (st) st.innerHTML = '';
|
||||
};
|
||||
box.querySelectorAll('input, select').forEach(el => {
|
||||
el.addEventListener('input', stale);
|
||||
el.addEventListener('change', stale);
|
||||
});
|
||||
const pw = this.container.querySelector('#sw-rs-pass');
|
||||
if (pw) pw.addEventListener('input', stale);
|
||||
sync();
|
||||
}
|
||||
|
||||
@ -1413,7 +1431,7 @@ class SetupWizard {
|
||||
return 'Give the repository URL.';
|
||||
}
|
||||
if (!pass || !pass.value) {
|
||||
return 'The repository password is needed to open the backup.';
|
||||
return 'Enter the backup password.';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@ -2350,6 +2368,19 @@ class SetupWizard {
|
||||
}
|
||||
// Only an edited exact path can be invalid; the scanned options all came
|
||||
// from the backend.
|
||||
// A restore cannot go past the Backup step on a promise: the next step
|
||||
// renders what is IN the backup, which means it has to have been opened.
|
||||
// Gated here rather than printed as an instruction — the step says what is
|
||||
// wrong at the moment the user tries to move on, instead of in advance and
|
||||
// forever.
|
||||
if (name === 'Backup') {
|
||||
const problem = this._restoreSourceProblem();
|
||||
if (problem) return problem;
|
||||
if (!this.restoreInfo) {
|
||||
return 'Select "Read this backup" first \u2014 we have to open it before we can show you what is inside.';
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'Storage') {
|
||||
const problem = this._syncStorageNav();
|
||||
if (problem) return problem;
|
||||
|
||||
@ -192,6 +192,30 @@ read -r -d '' DRIVE <<'JS'
|
||||
($('#sw-rs-verify-result') || {}).textContent || '');
|
||||
out.offersTheRealPath = !!$('#sw-rs-usesuggest');
|
||||
|
||||
// The step will not advance on a promise: the next one renders what is IN
|
||||
// the backup, so it has to have been opened. Gated rather than instructed —
|
||||
// the old copy told the user to enter a password beneath a field that had
|
||||
// one, and kept telling them after they had.
|
||||
const BACKUP = w.stepNames.indexOf('Backup');
|
||||
out.passwordLabel = ($('label[for="sw-rs-pass"]') || {}).textContent?.trim().split('\n')[0].trim();
|
||||
out.verdictHasNoInstruction =
|
||||
!/enter its password/i.test(($('#sw-rs-verify-result') || {}).textContent || '');
|
||||
|
||||
w.restoreInfo = null;
|
||||
$('#sw-rs-path').value = '/libreportal-backups/1';
|
||||
$('#sw-rs-pass').value = '';
|
||||
out.blockedWithoutPassword = !!w.validateStep(BACKUP);
|
||||
$('#sw-rs-pass').value = 'x';
|
||||
out.blockedUntilRead = !!w.validateStep(BACKUP);
|
||||
w.restoreInfo = { host: 'h', hosts: ['h'], system: { present: true, date: '', domains: [] }, apps: [] };
|
||||
out.allowedOnceRead = !w.validateStep(BACKUP);
|
||||
|
||||
// Editing the source after a read makes that read stale — otherwise the
|
||||
// wizard carries the old repository's contents forward under a new path.
|
||||
$('#sw-rs-path').value = '/somewhere/else';
|
||||
$('#sw-rs-path').dispatchEvent(new Event('input', { bubbles: true }));
|
||||
out.readInvalidatedByEdit = !w.restoreInfo;
|
||||
|
||||
// A password must leave as a reference and not linger in the DOM. Stubbed:
|
||||
// the real channel is covered by lp-secret-channel-test, and what matters
|
||||
// here is that readBackup routes through it at all rather than putting the
|
||||
@ -334,6 +358,14 @@ chk "placeholder is a real path" "$(g .placeholderIsReal)" true
|
||||
chk "the parent-folder mistake is explained" "$(g .parentFolderExplained)" true
|
||||
chk "and the real path is offered" "$(g .offersTheRealPath)" true
|
||||
|
||||
echo "the step gates rather than instructs"
|
||||
chk "the field is called Backup Password" "$(g .passwordLabel)" "Backup Password"
|
||||
chk "no 'enter its password' copy" "$(g .verdictHasNoInstruction)" true
|
||||
chk "blocked without a password" "$(g .blockedWithoutPassword)" true
|
||||
chk "blocked until the backup is read" "$(g .blockedUntilRead)" true
|
||||
chk "allowed once it has been" "$(g .allowedOnceRead)" true
|
||||
chk "editing the source invalidates it" "$(g .readInvalidatedByEdit)" true
|
||||
|
||||
echo "the password"
|
||||
chk "goes through the secret channel" "$(g .passwordWasStashed)" true
|
||||
chk "leaves the payload as a ref" "$(g .payloadCarriesRef)" true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user