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",