From 0a6dd75a05b3eb2cbbefbd2fe5e73c3ba8dd2bac Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 17 Feb 2021 18:07:55 +0100 Subject: [PATCH] Apply password policy on sign up screen --- assets/css/components/passwordInput.css | 3 + assets/css/dark_theme.css | 3 + assets/js/common/formChecker.js | 7 +- assets/js/common/shorcuts.js | 10 +- assets/js/components/manon.js | 4 +- assets/js/components/passwordInput.js | 209 ++++++++++++++++++++++++ assets/js/langs/en.inc.js | 2 - assets/js/langs/fr.inc.js | 2 - assets/js/pages/createAccount.js | 25 +-- assets/js/typings/ServerConfig.d.ts | 12 ++ system/config/dev.config.php | 8 + 11 files changed, 265 insertions(+), 20 deletions(-) create mode 100644 assets/css/components/passwordInput.css create mode 100644 assets/js/components/passwordInput.js diff --git a/assets/css/components/passwordInput.css b/assets/css/components/passwordInput.css new file mode 100644 index 00000000..21a1ebbe --- /dev/null +++ b/assets/css/components/passwordInput.css @@ -0,0 +1,3 @@ +/** + * Password input + */ \ No newline at end of file diff --git a/assets/css/dark_theme.css b/assets/css/dark_theme.css index c85b510a..4531dc90 100644 --- a/assets/css/dark_theme.css +++ b/assets/css/dark_theme.css @@ -91,6 +91,9 @@ fieldset[disabled] .form-control { } +.input-group .help-block { + color: var(--white); +} /** * Boxes diff --git a/assets/js/common/formChecker.js b/assets/js/common/formChecker.js index a1d668ce..ec11a9ec 100644 --- a/assets/js/common/formChecker.js +++ b/assets/js/common/formChecker.js @@ -3,7 +3,7 @@ * * @author Pierre HUBERT */ -ComunicWeb.common.formChecker = { +const FormChecker = { /** * Check an input @@ -73,4 +73,7 @@ ComunicWeb.common.formChecker = { return inputOK; } -}; \ No newline at end of file +}; + + +ComunicWeb.common.formChecker = FormChecker; \ No newline at end of file diff --git a/assets/js/common/shorcuts.js b/assets/js/common/shorcuts.js index 1f171446..a1ba4ed2 100644 --- a/assets/js/common/shorcuts.js +++ b/assets/js/common/shorcuts.js @@ -328,7 +328,15 @@ async function showInputTextDialog(title, message, defaultValue = "") { * Prepare for potential future translation system * * @param {String} input Input string + * @param {Object} arguments Arguments to apply to the string */ -function tr(input) { +function tr(input, values) { + + // Apply arguments + for (const key in values) { + if (Object.hasOwnProperty.call(values, key)) + input = input.replace("%"+key+"%", values[key]); + } + return input; } \ No newline at end of file diff --git a/assets/js/components/manon.js b/assets/js/components/manon.js index 75ed7165..0e997bb6 100644 --- a/assets/js/components/manon.js +++ b/assets/js/components/manon.js @@ -7,7 +7,7 @@ /** * Check if it is Manon's birthday */ -async function checkManonBirthday(force) { +/*async function checkManonBirthday(force) { if(force !== true) { // Manon's feature only @@ -69,4 +69,4 @@ document.addEventListener("wsOpen", () => { setTimeout(() => checkManonBirthday(), 1000); }, { once: true -}) \ No newline at end of file +})*/ \ No newline at end of file diff --git a/assets/js/components/passwordInput.js b/assets/js/components/passwordInput.js new file mode 100644 index 00000000..df63050c --- /dev/null +++ b/assets/js/components/passwordInput.js @@ -0,0 +1,209 @@ +/** + * Password input field + * + * @author Pierre Hubert + */ + +class PasswordInput { + + /** + * Create password input field + * + * @param {HTMLElement} target The target for the password input + * @param {String} label The label for the input + * @param {String} placeholder The placeholder to use + */ + constructor(target, label, placeholder) { + this._input = createFormGroup({ + target: target, + label: label, + placeholder: placeholder, + type: "password", + }); + + this._input.parentNode.parentNode.classList.add("password-input-group"); + + this._input.addEventListener("keyup", () => this._refreshArea()); + this._input.addEventListener("change", () => this._refreshArea()); + + this.helpArea = createElem2({ + appendTo: this._input.parentNode, + type: "span", + class: "help-block" + }); + + this._init(); + } + + async _init() { + try { + await ServerConfig.ensureLoaded() + this._ready = true; + this._valid = false; + this._refreshArea(); + } catch(e) { + console.error(e); + notify(tr("Failed to load server configuration! Please reload the page!"), "danger"); + } + } + + setFirstName(firstName) { + this._firstName = firstName; + this._refreshArea(); + } + + setLastName(lastName) { + this._lastName = lastName; + this._refreshArea(); + } + + setEmail(email) { + this._email = email.length > 2 ? email : null; + this._refreshArea(); + } + + isValid() { + this._refreshArea(); + return this._valid; + } + + _refreshArea() { + if (!this._ready) + return; + + this.helpArea.innerHTML = ""; + + /** @type {String} */ + const password = this._input.value.trim().toLowerCase(); + const policy = ServerConfig.conf.password_policy; + + if (password.length == 0) { + this._valid = false; + return; + } + + this._good = []; + this._bad = []; + + // Check email + this._performMandatoryCheck( + tr("Password must not contains part of email address"), + !policy.allow_email_in_password && this._email, + () => !password.includes(this._email.toLowerCase()) && !this._email.toLowerCase().includes(password) + ); + + // Check name + this._performMandatoryCheck( + tr("Password must not contains part of name"), + !policy.allow_name_in_password && this._firstName && this._lastName, + () => !password.includes(this._firstName.toLowerCase()) && !password.includes(this._lastName) + ); + + + // Check password length + this._performMandatoryCheck( + tr("Password must be composed of at least %num_chars% characters", {num_chars: policy.min_password_length}), + true, + () => password.length >= policy.min_password_length + ); + + // Check if mandatory arguments are respected + this._valid = this._bad.length == 0; + + + // Check categories presence + if(policy.min_categories_presence > 0) { + + let count = 0 + + this._performCategoryCheck( + this._input.value, + tr("At least %num% upper case character", {num: policy.min_number_upper_case_letters}), + "[A-Z]", + policy.min_number_upper_case_letters + ) && count++ + + this._performCategoryCheck( + this._input.value, + tr("At least %num% lower case character", {num: policy.min_number_lower_case_letters}), + "[a-z]", + policy.min_number_lower_case_letters + ) && count++ + + + this._performCategoryCheck( + password, + tr("At least %num% digit character", {num: policy.min_number_digits}), + "[0-9]", + policy.min_number_digits + ) && count++ + + + this._performCategoryCheck( + password, + tr("At least %num% special character", {num: policy.min_number_special_characters}), + "[^0-9a-zA-Z]", + policy.min_number_special_characters + ) && count++ + + if (count < policy.min_categories_presence) + this._valid = false; + + if (policy.min_categories_presence < 4) + this._performMandatoryCheck( + tr( + "At least %num% of the following : upper case letter, lower case letter, digit or special character", + {num: policy.min_categories_presence} + ), + true, + () => count >= policy.min_categories_presence + ); + } + + + if (this._valid) { + this.helpArea.innerHTML = tr("You can use this password"); + return; + } + + for(let bad of this._bad) + this._addTip(false, bad); + + for(let good of this._good) + this._addTip(true, good); + } + + _performMandatoryCheck(check_label, requisite, check) { + if (!requisite) + return; + + const pass = check(); + if (!pass) + this._bad.push(check_label); + + else + this._good.push(check_label); + + return pass; + } + + _performCategoryCheck(password, check_label, regex, numRequired) { + if (numRequired < 1) + return true; + + const pass = [...password.matchAll(regex)].length >= numRequired; + + if (!pass) + this._bad.push(check_label); + + else + this._good.push(check_label); + + return pass; + + } + + _addTip(isGood, content) { + this.helpArea.innerHTML += " " + content + "
"; + } +} diff --git a/assets/js/langs/en.inc.js b/assets/js/langs/en.inc.js index cd022c2e..10159ced 100644 --- a/assets/js/langs/en.inc.js +++ b/assets/js/langs/en.inc.js @@ -199,8 +199,6 @@ ComunicWeb.common.langs.en = { form_create_account_last_name_placeholder: "Your last name", form_create_account_email_address_label: "Email address Warning! You will not be able to change this later !", form_create_account_email_address_placeholder: "Your email address", - form_create_account_password_label: "Password", - form_create_account_password_placeholder: "Your password", form_create_account_confirm_password_label: "Confirm your password", form_create_account_confirm_password_placeholder: "Your password", form_create_account_terms_label: "I have read and accepted the terms of use of the network", diff --git a/assets/js/langs/fr.inc.js b/assets/js/langs/fr.inc.js index 59e93f37..3e7501fa 100644 --- a/assets/js/langs/fr.inc.js +++ b/assets/js/langs/fr.inc.js @@ -197,8 +197,6 @@ ComunicWeb.common.langs.fr = { form_create_account_last_name_placeholder: "Votre nom", form_create_account_email_address_label: "Adresse mail Attention ! Vous ne pourrez pas changer cette valeur plus tard !", form_create_account_email_address_placeholder: "Votre adresse mail", - form_create_account_password_label: "Mot de passe", - form_create_account_password_placeholder: "Votre mot de passe", form_create_account_confirm_password_label: "Confirmez votre mot de passe", form_create_account_confirm_password_placeholder: "Votre mot de passe", form_create_account_terms_label: "J'ai lu et accepté les conditions d'utilisation du réseau", diff --git a/assets/js/pages/createAccount.js b/assets/js/pages/createAccount.js index ee7430d4..881ca80d 100644 --- a/assets/js/pages/createAccount.js +++ b/assets/js/pages/createAccount.js @@ -77,13 +77,16 @@ ComunicWeb.pages.createAccount = { type: "email" }); - //Input user password - var passwordInput = createFormGroup({ - target: formRoot, - label: lang("form_create_account_password_label"), - placeholder: lang("form_create_account_password_placeholder"), - type: "password" - }); + // Input user password + const passwordInput = new PasswordInput(formRoot, tr("Password"), tr("Your password")); + emailInput.addEventListener("keyup", () => passwordInput.setEmail(emailInput.value)) + emailInput.addEventListener("change", () => passwordInput.setEmail(emailInput.value)) + + firstNameInput.addEventListener("keyup", () => passwordInput.setFirstName(firstNameInput.value)) + firstNameInput.addEventListener("change", () => passwordInput.setFirstName(firstNameInput.value)) + + lastNameInput.addEventListener("keyup", () => passwordInput.setLastName(lastNameInput.value)) + lastNameInput.addEventListener("change", () => passwordInput.setLastName(lastNameInput.value)) //Confirm user password var confirmPasswordInput = createFormGroup({ @@ -142,19 +145,19 @@ ComunicWeb.pages.createAccount = { return notify(lang("form_create_account_err_need_accept_terms"), "danger"); //Check the first name - if(!ComunicWeb.common.formChecker.checkInput(firstNameInput, true)) + if(!FormChecker.checkInput(firstNameInput, true)) return notify(lang("form_create_account_err_need_first_name"), "danger"); //Check the last name - if(!ComunicWeb.common.formChecker.checkInput(lastNameInput, true)) + if(!FormChecker.checkInput(lastNameInput, true)) return notify(lang("form_create_account_err_check_last_name"), "danger"); //Check the email address - if(!ComunicWeb.common.formChecker.checkInput(emailInput, true)) + if(!FormChecker.checkInput(emailInput, true)) return notify(lang("form_create_account_err_check_email_address"), "danger"); //Check the password - if(!ComunicWeb.common.formChecker.checkInput(passwordInput, true)) + if(!passwordInput._valid) return notify(lang("form_create_account_err_check_password"), "danger"); //Check the confirmation password diff --git a/assets/js/typings/ServerConfig.d.ts b/assets/js/typings/ServerConfig.d.ts index 02c604a7..27ff7331 100644 --- a/assets/js/typings/ServerConfig.d.ts +++ b/assets/js/typings/ServerConfig.d.ts @@ -4,6 +4,17 @@ * @author Pierre Hubert */ +declare interface PasswordPolicy { + allow_email_in_password: boolean, + allow_name_in_password: boolean, + min_password_length: number, + min_number_upper_case_letters: number, + min_number_lower_case_letters: number, + min_number_digits: number, + min_number_special_characters: number, + min_categories_presence: number, +} + declare interface DataConservationPolicySettings { min_inactive_account_lifetime: number, min_notification_lifetime: number, @@ -14,5 +25,6 @@ declare interface DataConservationPolicySettings { } declare interface StaticServerConfig { + password_policy: PasswordPolicy, data_conservation_policy: DataConservationPolicySettings; } \ No newline at end of file diff --git a/system/config/dev.config.php b/system/config/dev.config.php index b6b7fad4..af304648 100644 --- a/system/config/dev.config.php +++ b/system/config/dev.config.php @@ -237,6 +237,9 @@ class Dev { //Pacman (easter egg) stylesheet "css/components/pacman.css", + // Password input + "css/components/passwordInput.css", + //Pages stylesheets //User Page "css/pages/userPage/main.css", @@ -469,8 +472,13 @@ class Dev { //Pacman component (easter egg) "js/components/pacman.js", + + // Manon "js/components/manon.js", + // Password input + "js/components/passwordInput.js", + //User scripts "js/user/loginTokens.js", "js/user/userLogin.js",