backup-reauth: the account password before a backup or an export

Agreed with the owner on 2026-10-13, taken up on 2026-10-16 ("Да"); the
mockup is 1888dce. Both secret downloads - the full backup on Server ›
Backup and a domain's export on its settings - ask for "Your account
password": reauth (handlers_backup.go) checks the signed-in user's own
password with bcrypt, as a password change does, under the same limiter
(auth.AllowPasswordChange: ten in fifteen minutes per user, shared with the
Account form). A wrong or missing one is 401 on the same page with "Your
account password is incorrect.", logged; nothing is sent. The import and
the selfpost-backup CLI are not asked.

Also, the owner's phone screenshot of 2026-10-16 ("кнопка выбора файла не
такая, как в макете. Это косяк?"): the import's file field was a bare
<input type="file"> drawn by the browser ("Browse…"), where the mockup has
Bulma's .file - "Choose a file…" and the name chosen. The template now
follows the mockup; panel.js writes the name into .file-name on change and
at load (testdata/filename_test.js, run by TestPanelJS). No other file
field exists.

Tests: TestSecretDownloadsAskForTheAccountPassword (missing, wrong, right,
the limiter), the export refusals of the settings page with the password,
cmd/panel's restore tests and e2e's backups with it, e2e's refusal of a
backup without and with a wrong password. go vet + go test (26 packages)
and make e2e on the test server for this tree.

Docs: security.md § Backup and domain export, guide § Full backup and
restore, help (word for word with the mockup); CHANGELOG [Unreleased];
roadmap row done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-16 22:39:13 +03:00
co-authored by Claude Opus 5.5
parent 1888dce1d4
commit 49c872f836
16 changed files with 217 additions and 11 deletions
+50
View File
@@ -0,0 +1,50 @@
// The name of the file chosen in a Bulma .file field is written into its
// .file-name (the import of a domain on Server › Backup): on change, and at
// load for a page the browser restored with a file still chosen. Run by
// TestPanelJS (paneljs_test.go): node filename_test.js <panel.js>. Exits
// non-zero with what went wrong.
"use strict";
const fs = require("fs");
const vm = require("vm");
const failures = [];
function check(cond, what) {
if (!cond) failures.push(what);
}
const listeners = {};
const label = { textContent: "No file chosen" };
const box = { querySelector: (sel) => (sel === ".file-name" ? label : null) };
const input = {
files: [],
matches: (sel) => sel === "input.file-input",
closest: (sel) => (sel === ".file" ? box : null),
};
const document = {
hidden: false,
addEventListener: (type, fn) => (listeners[type] = listeners[type] || []).push(fn),
body: { addEventListener() {} },
getElementById: () => null,
querySelectorAll: (sel) => (sel === "input.file-input" ? [input] : []),
};
vm.runInNewContext(fs.readFileSync(process.argv[2], "utf8"), {
document, window: { addEventListener() {} }, htmx: { config: {}, ajax() {} }, setTimeout, clearTimeout, Date, Object, Math, console,
});
const fire = (type, ev) => (listeners[type] || []).forEach((fn) => fn(ev));
input.files = [{ name: "example.org.spde" }];
fire("change", { target: input });
check(label.textContent === "example.org.spde", `after choosing a file the name reads ${JSON.stringify(label.textContent)}`);
input.files = [];
fire("change", { target: input });
check(label.textContent === "No file chosen", `after clearing the choice the name reads ${JSON.stringify(label.textContent)}`);
input.files = [{ name: "restored.json" }];
fire("DOMContentLoaded", {});
check(label.textContent === "restored.json", `a restored page shows ${JSON.stringify(label.textContent)}`);
if (failures.length) {
console.error(failures.join("\n"));
process.exit(1);
}