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>
51 lines
1.9 KiB
JavaScript
51 lines
1.9 KiB
JavaScript
// 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);
|
||
}
|