webui: password webmode as a panel

This commit is contained in:
Maxim Prokhorov
2024-07-12 19:00:06 +03:00
parent ed26dbda8b
commit 72fbdc2425
7 changed files with 214 additions and 149 deletions

View File

@@ -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;
}