feat(panel): detect import encryption from file extension, not a checkbox

The import form used to ask users to tick "the file is encrypted" before
showing the password field, even though the server already decides purely
from the envelope magic bytes. Reveal the password field automatically for
a .spde file (hide it for .json) so the checkbox is no longer needed.
This commit is contained in:
2026-08-07 04:30:24 +03:00
parent c0c66dfcdb
commit 9745edd132
4 changed files with 45 additions and 14 deletions
+1
View File
@@ -117,6 +117,7 @@ func TestBackupPageOffersEncryption(t *testing.T) {
for _, want := range []string{ for _, want := range []string{
`name="encrypt"`, `name="password"`, `name="password_confirm"`, `name="encrypt"`, `name="password"`, `name="password_confirm"`,
`name="import_password"`, "data-encrypt-toggle", "data-encrypt-fields", `name="import_password"`, "data-encrypt-toggle", "data-encrypt-fields",
"data-import-password-fields",
fmt.Sprintf("at least %d characters", minSecretFilePasswordLen), fmt.Sprintf("at least %d characters", minSecretFilePasswordLen),
"The two passwords do not match.", "The two passwords do not match.",
} { } {
+8 -4
View File
@@ -335,20 +335,24 @@ button.copy { flex: none; margin-top: 0.3rem; }
} }
.actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: var(--danger-bg-hover); } .actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: var(--danger-bg-hover); }
/* The optional "encrypt this download" block on the backup, export and import /* The optional "encrypt this download" block on the backup and export forms.
forms. Its label is the one checkbox in the panel, so it opts out of the Its label is the one checkbox in the panel, so it opts out of the
block-level label rule above and sits on one line with its box; the fields it block-level label rule above and sits on one line with its box; the fields it
reveals are indented under it to read as its consequence rather than as three reveals are indented under it to read as its consequence rather than as three
more fields of the form. panel.js hides the inner block until the box is more fields of the form. panel.js hides the inner block until the box is
ticked (and empties it when unticked); without JavaScript everything stays ticked (and empties it when unticked); without JavaScript everything stays
visible, which the server handles identically. */ visible, which the server handles identically. The import form reuses the
same indented .encrypt-fields look for its password field, but reveals it
by file extension instead of a checkbox (see panel.js). */
.encrypt { margin-top: 1.2rem; } .encrypt { margin-top: 1.2rem; }
.encrypt label.check { .encrypt label.check {
display: flex; align-items: center; gap: 0.5rem; margin: 0; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; margin: 0; font-weight: 600;
} }
.encrypt label.check input { width: auto; margin: 0; } .encrypt label.check input { width: auto; margin: 0; }
.encrypt-fields { .encrypt-fields {
margin-left: 1.6rem; padding-left: 0.9rem; border-left: 2px solid var(--border); margin-top: 1.2rem; margin-left: 1.6rem; padding-left: 0.9rem;
border-left: 2px solid var(--border);
} }
.encrypt .encrypt-fields { margin-top: 0; }
.encrypt-fields label { margin-top: 0.7rem; } .encrypt-fields label { margin-top: 0.7rem; }
.encrypt-fields .muted { margin: 0.5rem 0 0; font-size: 0.85rem; } .encrypt-fields .muted { margin: 0.5rem 0 0; font-size: 0.85rem; }
+32
View File
@@ -102,6 +102,37 @@
}); });
} }
// --- Import password field shown based on the chosen file's extension ---
// The domain-import file decides for itself whether it is encrypted (the
// server checks the envelope magic, not a checkbox), so the panel offers
// the password field the same way: reveal it for a .spde file, hide and
// clear it for a plain .json one. An unrecognised name leaves the field
// visible rather than guessing wrong and hiding a password the file needs.
function syncImportPasswordField(input) {
var form = input.closest("form");
var fields = form && form.querySelector("[data-import-password-fields]");
if (!fields) {
return;
}
var name = (input.files && input.files[0] && input.files[0].name || "").toLowerCase();
var hide = name !== "" && /\.json$/.test(name);
fields.hidden = hide;
if (hide) {
fields.querySelectorAll("input").forEach(function (pw) {
pw.value = "";
});
}
}
function initImportPasswordField(root) {
root.querySelectorAll("input[data-import-file]").forEach(function (input) {
syncImportPasswordField(input);
input.addEventListener("change", function () {
syncImportPasswordField(input);
});
});
}
// --- Section index follows the page ----------------------------------- // --- Section index follows the page -----------------------------------
// The long pages list their own sections in the navigation column (the // The long pages list their own sections in the navigation column (the
// "sections" template). Marking the one currently in view turns that list // "sections" template). Marking the one currently in view turns that list
@@ -167,6 +198,7 @@
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
initAddressFields(document); initAddressFields(document);
initEncryptFields(document); initEncryptFields(document);
initImportPasswordField(document);
initSectionIndex(); initSectionIndex();
}); });
+2 -8
View File
@@ -28,17 +28,11 @@
{{if .ImportErr}}<p class="error">{{.ImportErr}}</p>{{end}} {{if .ImportErr}}<p class="error">{{.ImportErr}}</p>{{end}}
<form method="post" action="/domains/import" enctype="multipart/form-data"> <form method="post" action="/domains/import" enctype="multipart/form-data">
<label for="importfile">Domain export file</label> <label for="importfile">Domain export file</label>
<input id="importfile" name="file" type="file" accept=".json,.spde,application/json" required> <input id="importfile" name="file" type="file" accept=".json,.spde,application/json" required data-import-file>
<div class="encrypt"> <div class="encrypt-fields" data-import-password-fields>
<label class="check">
<input type="checkbox" data-encrypt-toggle>
<span>The file is encrypted (<code>.spde</code>)</span>
</label>
<div class="encrypt-fields" data-encrypt-fields>
<label for="importpw">Password</label> <label for="importpw">Password</label>
<input id="importpw" name="import_password" type="password" autocomplete="off"> <input id="importpw" name="import_password" type="password" autocomplete="off">
</div> </div>
</div>
<button type="submit">Import domain</button> <button type="submit">Import domain</button>
</form> </form>
</div> </div>