diff --git a/code/html/src/index.css b/code/html/src/index.css
index 34ea2d3b..640ac4c8 100644
--- a/code/html/src/index.css
+++ b/code/html/src/index.css
@@ -2,6 +2,35 @@
General
-------------------------------------------------------------------------- */
+#layout.initial {
+ padding-left: 0;
+}
+
+#layout.initial .content {
+ margin: 0 auto;
+}
+
+#layout.initial .menu-link,
+#layout.initial #menu {
+ display: none;
+}
+
+#layout.initial .button-setup-password {
+ display: inline-block;
+}
+
+#layout .button-setup-password {
+ display: none;
+}
+
+#layout.initial .setup-password-initial {
+ display: block;
+}
+
+#layout .setup-password-initial {
+ display: none;
+}
+
#menu .pure-menu-heading {
font-size: 100%;
padding: .5em .5em;
@@ -98,10 +127,6 @@ div.center {
display: none;
}
-#password .content {
- margin: 0 auto;
-}
-
#layout .content {
margin: 0;
}
@@ -229,6 +254,7 @@ label {
.button-clear-messages,
.button-clear-counts,
.button-settings-factory,
+.button-settings-password,
.button-setup-password.button-setup-lenient {
background: rgb(192, 0, 0); /* redish */
}
diff --git a/code/html/src/index.html b/code/html/src/index.html
index bf48f6b5..ea7562e4 100644
--- a/code/html/src/index.html
+++ b/code/html/src/index.html
@@ -19,14 +19,6 @@
-
-
+
+
diff --git a/code/html/src/index.mjs b/code/html/src/index.mjs
index f61223c8..87be0607 100644
--- a/code/html/src/index.mjs
+++ b/code/html/src/index.mjs
@@ -7,9 +7,10 @@ window.addEventListener("error", (event) => {
});
import {
+ onPanelTargetClick,
pageReloadIn,
randomString,
- onPanelTargetClick,
+ showPanelByName,
styleInject,
} from './core.mjs';
@@ -19,10 +20,8 @@ import { askAndCall } from './question.mjs';
import {
init as initSettings,
- applySettings,
+ applySettingsFromForms,
askSaveSettings,
- getData,
- setChangedElement,
updateVariables,
variableListeners,
} from './settings.mjs';
@@ -143,16 +142,13 @@ function onMessage(value) {
* @param {number} value
*/
function initWebMode(value) {
- const initial = (1 === value);
+ const layout = /** @type {!HTMLElement} */
+ (document.getElementById("layout"));
+ layout.style.display = "inherit";
- const layout = document.getElementById("layout")
- if (layout) {
- layout.style.display = (initial ? "none" : "inherit");
- }
-
- const password = document.getElementById("password");
- if (password) {
- password.style.display = initial ? "inherit" : "none";
+ if (1 === value) {
+ layout.classList.add("initial");
+ showPanelByName("password");
}
}
@@ -326,9 +322,9 @@ function generatePasswordsForForm(form) {
.map((x) => form.elements.namedItem(x))
.filter((x) => x instanceof HTMLInputElement)
.forEach((elem) => {
- setChangedElement(elem);
elem.type = "text";
elem.value = value;
+ elem.dispatchEvent(new Event("change"));
});
}
@@ -341,14 +337,14 @@ function initSetupPassword(form) {
elem.addEventListener("click", (event) => {
event.preventDefault();
- const target = /** @type {HTMLInputElement} */
+ const target = /** @type {!HTMLInputElement} */
(event.target);
- const lenient = target.classList
- .contains("button-setup-lenient");
+ const strict = target.classList
+ .contains("button-setup-strict");
const forms = [form];
- if (validateFormsPasswords(forms, {strict: !lenient})) {
- applySettings(getData(forms, {cleanup: false}));
+ if (validateFormsPasswords(forms, {strict})) {
+ applySettingsFromForms(forms);
}
});
});
@@ -433,11 +429,9 @@ function onJsonPayload(event) {
}
function init() {
- // Initial page, when webMode only allows to change the password
- const passwd = document.forms.namedItem("form-setup-password");
- if (passwd) {
- initSetupPassword(passwd);
- }
+ const password = /** @type {!HTMLFormElement} */
+ (document.forms.namedItem("form-setup-password"));
+ initSetupPassword(password);
document.querySelectorAll(".password-reveal")
.forEach((elem) => {
diff --git a/code/html/src/panel-admin.html b/code/html/src/panel-admin.html
index 1378783c..6a88096f 100644
--- a/code/html/src/panel-admin.html
+++ b/code/html/src/panel-admin.html
@@ -19,26 +19,11 @@
diff --git a/code/html/src/settings.mjs b/code/html/src/settings.mjs
index 26163432..1314583f 100644
--- a/code/html/src/settings.mjs
+++ b/code/html/src/settings.mjs
@@ -1,6 +1,16 @@
import { notifyError } from './errors.mjs';
-import { pageReloadIn, count } from './core.mjs';
-import { send, sendAction, connectionUrls } from './connection.mjs';
+import {
+ count,
+ pageReloadIn,
+ showPanelByName,
+} from './core.mjs';
+
+import {
+ send,
+ sendAction,
+ connectionUrls,
+} from './connection.mjs';
+
import { validateForms } from './validate.mjs';
/**
@@ -13,12 +23,17 @@ export function isChangedElement(elem) {
/**
* @param {Element} node
*/
-export function countChangedElements(node) {
- const elems = /** @type {Array} */
- (Array.from(node.querySelectorAll(
+export function getElements(node) {
+ return /** @type {Array} */(
+ Array.from(node.querySelectorAll(
"input[data-changed],select[data-changed]")));
+}
- return count(elems, isChangedElement);
+/**
+ * @param {Element} node
+ */
+export function countChangedElements(node) {
+ return count(getElements(node), isChangedElement);
}
/**
@@ -1271,18 +1286,24 @@ export function applySettings(settings) {
send(JSON.stringify({settings}));
}
-export function applySettingsFromAllForms() {
+/** @param {HTMLFormElement[]} forms */
+export function applySettingsFromForms(forms) {
+ applySettings(getData(forms));
+ Settings.resetChanged();
+ waitForSaved();
+}
+
+/** @param {Event} event */
+function applySettingsFromAllForms(event) {
+ event.preventDefault();
+
const elems = /** @type {NodeListOf} */
(document.querySelectorAll("form.form-settings"));
const forms = Array.from(elems);
if (validateForms(forms)) {
- applySettings(getData(forms));
- Settings.resetChanged();
- waitForSaved();
+ applySettingsFromForms(forms);
}
-
- return false;
}
/** @param {Event} event */
@@ -1373,10 +1394,7 @@ export function init() {
?.addEventListener("change", handleSettingsFile);
document.querySelector(".button-save")
- ?.addEventListener("click", (event) => {
- event.preventDefault();
- applySettingsFromAllForms();
- });
+ ?.addEventListener("click", applySettingsFromAllForms);
document.querySelector(".button-settings-backup")
?.addEventListener("click", (event) => {
@@ -1401,6 +1419,11 @@ export function init() {
document.querySelector(".button-settings-factory")
?.addEventListener("click", resetToFactoryDefaults);
+ document.querySelector(".button-settings-password")
+ ?.addEventListener("click", () => {
+ showPanelByName("password");
+ });
+
document.querySelectorAll(".button-add-settings-group")
.forEach((elem) => {
elem.addEventListener("click", onGroupSettingsAddClick);
diff --git a/code/html/src/validate.mjs b/code/html/src/validate.mjs
index d6ab5748..7342a85d 100644
--- a/code/html/src/validate.mjs
+++ b/code/html/src/validate.mjs
@@ -1,10 +1,19 @@
-import { isChangedElement } from './settings.mjs';
+import { isChangedElement, getElements } from './settings.mjs';
+
+// per. [RFC1035](https://datatracker.ietf.org/doc/html/rfc1035)
+const INVALID_HOSTNAME = `
+Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'),
+the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.`;
+
+const INVALID_PASSWORD = "Invalid password!";
+const DIFFERENT_PASSWORD = "Passwords are different!";
+const EMPTY_PASSWORD = "Password cannot be empty!";
/**
- * @param {string} password
+ * @param {string} value
* @returns {boolean}
*/
-export function validatePassword(password) {
+export function validatePassword(value) {
// http://www.the-art-of-web.com/javascript/validate-password/
// at least one lowercase and one uppercase letter or number
// at least eight characters (letters, numbers or special characters)
@@ -14,89 +23,118 @@ export function validatePassword(password) {
// https://github.com/xoseperez/espurna/issues/1151
const Pattern = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*()<>,.?;:{}[\]\\|]{8,63}/;
- return ((typeof password === "string")
- && (password.length >= 8)
- && Pattern.test(password));
+ return ((typeof value === "string")
+ && (value.length >= 8)
+ && Pattern.test(value));
+}
+
+/**
+ * @typedef {{strict?: boolean}} ValidationOptions
+ */
+
+/**
+ * @param {HTMLInputElement[]} pair
+ * @param {ValidationOptions} options
+ * @returns {boolean}
+ */
+function validatePasswords(pair, {strict = true} = {}) {
+ if (pair.length !== 2) {
+ alert(EMPTY_PASSWORD);
+ return false;
+ }
+
+ if (pair.some((x) => !x.value.length)) {
+ alert(EMPTY_PASSWORD);
+ return false;
+ }
+
+ if (pair[0].value !== pair[1].value) {
+ alert(DIFFERENT_PASSWORD);
+ return false;
+ }
+
+ /** @param {HTMLInputElement} elem */
+ function checkValidity(elem) {
+ if (!elem.checkValidity()) {
+ return false;
+ }
+
+ return !strict || validatePassword(elem.value);
+ }
+
+ if (pair.every(checkValidity)) {
+ return true;
+ }
+
+ alert(INVALID_PASSWORD);
+ return false;
+
}
/**
* Try to validate 'adminPass{0,1}', searching the first form containing both.
* In case it's default webMode, avoid checking things when both fields are empty (`required === false`)
* @param {HTMLFormElement[]} forms
- * @param {{required?: boolean, strict?: boolean}} options
+ * @param {ValidationOptions} options
* @returns {boolean}
*/
-export function validateFormsPasswords(forms, {required = true, strict = true} = {}) {
- const [first, second] = Array.from(forms)
- .flatMap((x) => {
- return [
- x.elements.namedItem("adminPass0"),
- x.elements.namedItem("adminPass1"),
- ];
- })
+export function validateFormsPasswords(forms, {strict = true} = {}) {
+ const pair = Array.from(forms)
+ .flatMap((x) => [
+ x.elements.namedItem("adminPass0"),
+ x.elements.namedItem("adminPass1"),
+ ])
.filter((x) => x instanceof HTMLInputElement);
- if (first && second) {
- if (!required && !first.value.length && !second.value.length) {
- return true;
- }
-
- if (first.value !== second.value) {
- alert("Passwords are different!");
- return false;
- }
-
- const firstValid = first.checkValidity()
- && (!strict || validatePassword(first.value));
- const secondValid = second.checkValidity()
- && (!strict || validatePassword(second.value));
-
- if (firstValid && secondValid) {
- return true;
- }
- }
-
- alert(`Invalid password!`);
-
- return false;
-}
-
-/**
- * Same as above, but only applies to the general settings page.
- * Find the first available form that contains 'hostname' input
- * @param {HTMLFormElement[]} forms
- */
-export function validateFormsHostname(forms) {
- // per. [RFC1035](https://datatracker.ietf.org/doc/html/rfc1035)
- // Hostname may contain:
- // - the ASCII letters 'a' through 'z' (case-insensitive),
- // - the digits '0' through '9', and the hyphen.
- // Hostname labels cannot begin or end with a hyphen.
- // No other symbols, punctuation characters, or blank spaces are permitted.
- const [hostname] = Array.from(forms)
- .flatMap(form => form.elements.namedItem("hostname"))
- .filter((x) => x instanceof HTMLInputElement);
- if (!hostname) {
- return true;
- }
-
- // Validation pattern is attached to the element itself, so just check that.
- // (and, we also re-use the hostname for fallback SSID, thus limited to 1...32 chars instead of 1...63)
-
- const result = (hostname.value.length > 0)
- && (!isChangedElement(hostname) || hostname.checkValidity());
- if (!result) {
- alert(`Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'),
- the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.`);
- }
-
- return result;
+ return validatePasswords(pair, {strict});
}
/**
* @param {HTMLFormElement[]} forms
*/
export function validateForms(forms) {
- return validateFormsPasswords(forms, {strict: false})
- && validateFormsHostname(forms);
+ const elems = forms
+ .flatMap((form) => getElements(form))
+ .filter(isChangedElement)
+
+ if (!elems.length) {
+ return false;
+ }
+
+ /** @type {HTMLInputElement[]} */
+ const passwords = [];
+
+ for (let elem of elems) {
+ switch (elem.name) {
+ case "hostname":
+ if (!elem.checkValidity()) {
+ alert(INVALID_HOSTNAME);
+ return false;
+ }
+ break;
+
+ case "adminPass0":
+ case "adminPass1":
+ if (!(elem instanceof HTMLInputElement)) {
+ return false;
+ }
+
+ passwords.push(elem);
+ break;
+
+ default:
+ if (!elem.checkValidity()) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ if ((passwords.length > 0)
+ && (!validatePasswords(passwords, {strict: false})))
+ {
+ return false;
+ }
+
+ return true;
}