From a23fd3ee4be0d9cfd9fa291fc569ecd9b12945d9 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 7 Aug 2020 18:35:12 +0200 Subject: [PATCH] Improved the UI/UX for the password strength estimations --- .../templates/passwordStrengthLanguage.tpl | 1 + .../templates/passwordStrengthLanguage.tpl | 1 + .../Core/Ui/User/PasswordStrength.js | 114 +++++++++++------- wcfsetup/install/files/style/layout/form.scss | 63 ++++++++++ wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + 6 files changed, 137 insertions(+), 44 deletions(-) diff --git a/com.woltlab.wcf/templates/passwordStrengthLanguage.tpl b/com.woltlab.wcf/templates/passwordStrengthLanguage.tpl index a147d52280..c40d4fe55f 100644 --- a/com.woltlab.wcf/templates/passwordStrengthLanguage.tpl +++ b/com.woltlab.wcf/templates/passwordStrengthLanguage.tpl @@ -1,4 +1,5 @@ Language.addObject({ + 'wcf.user.password.strength': '{lang}wcf.user.password.strength{/lang}', 'wcf.user.password.zxcvbn.suggestions.use_words_avoid_common_phrases': '{lang}wcf.user.password.zxcvbn.suggestions.use_words_avoid_common_phrases{/lang}', 'wcf.user.password.zxcvbn.suggestions.no_need_for_symbols_digits_uppercase': '{lang}wcf.user.password.zxcvbn.suggestions.no_need_for_symbols_digits_uppercase{/lang}', 'wcf.user.password.zxcvbn.suggestions.add_word_uncommon_better': '{lang}wcf.user.password.zxcvbn.suggestions.add_word_uncommon_better{/lang}', diff --git a/wcfsetup/install/files/acp/templates/passwordStrengthLanguage.tpl b/wcfsetup/install/files/acp/templates/passwordStrengthLanguage.tpl index a147d52280..c40d4fe55f 100644 --- a/wcfsetup/install/files/acp/templates/passwordStrengthLanguage.tpl +++ b/wcfsetup/install/files/acp/templates/passwordStrengthLanguage.tpl @@ -1,4 +1,5 @@ Language.addObject({ + 'wcf.user.password.strength': '{lang}wcf.user.password.strength{/lang}', 'wcf.user.password.zxcvbn.suggestions.use_words_avoid_common_phrases': '{lang}wcf.user.password.zxcvbn.suggestions.use_words_avoid_common_phrases{/lang}', 'wcf.user.password.zxcvbn.suggestions.no_need_for_symbols_digits_uppercase': '{lang}wcf.user.password.zxcvbn.suggestions.no_need_for_symbols_digits_uppercase{/lang}', 'wcf.user.password.zxcvbn.suggestions.add_word_uncommon_better': '{lang}wcf.user.password.zxcvbn.suggestions.add_word_uncommon_better{/lang}', diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js index 48900cbefe..3207220db9 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js @@ -1,110 +1,136 @@ /** * Adds a password strength meter to a password input and exposes * zxcbn's verdict as sibling input. - * + * * @author Tim Duesterhus * @copyright 2001-2020 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Ui/User/PasswordStrength */ -define(['zxcvbn', 'Core', 'Language'], function(zxcvbn, Core, Language) { - "use strict"; +define(['zxcvbn', 'Core', 'Language', 'StringUtil'], function (zxcvbn, Core, Language, StringUtil) { + 'use strict'; var STATIC_DICTIONARY = []; if (elBySel('meta[property="og:site_name"]')) { STATIC_DICTIONARY.push(elBySel('meta[property="og:site_name"]').getAttribute('content')); } - function createMeter(options) { - var meter = elCreate('meter'); - Object.keys(options).forEach(function (key) { - meter[key] = options[key]; - }); - return meter; - } - function flatMap(array, callback) { return array.map(callback).reduce(function (carry, item) { return carry.concat(item); - }, []) + }, []); } function splitIntoWords(value) { - return [].concat( - value, - value.split(/\W+/) - ); + return [].concat(value, value.split(/\W+/)); } /** * @constructor */ function PasswordStrength(input, options) { this.init(input, options); } + PasswordStrength.prototype = { /** + * @param {Element} input * @param {object} options */ - init: function(input, options) { + init: function (input, options) { this._input = input; this._options = Core.extend({ relatedInputs: [], - staticDictionary: [], + staticDictionary: [] }, options); if (!this._options.feedbacker) { var phrases = Core.extend({}, zxcvbn.Feedback.default_phrases); for (var type in phrases) { - for (var phrase in phrases[type]) { - var languageItem = 'wcf.user.password.zxcvbn.' + type + '.' + phrase; - var value = Language.get(languageItem); - if (value !== languageItem) { - phrases[type][phrase] = value; + if (phrases.hasOwnProperty(type)) { + for (var phrase in phrases[type]) { + if (phrases[type].hasOwnProperty(phrase)) { + var languageItem = 'wcf.user.password.zxcvbn.' + type + '.' + phrase; + var value = Language.get(languageItem); + if (value !== languageItem) { + phrases[type][phrase] = value; + } + } } } } this._options.feedbacker = new zxcvbn.Feedback(phrases); } - this._meter = createMeter({ - min: 0, - max: 4, - low: 2, - high: 3, - optimum: 4 - }); - this._input.parentNode.insertBefore(this._meter, this._input.nextSibling); + this._wrapper = elCreate('div'); + this._wrapper.className = 'inputAddon inputAddonPasswordStrength'; + this._input.parentNode.insertBefore(this._wrapper, this._input); + this._wrapper.appendChild(this._input); + + var passwordStrengthWrapper = elCreate('div'); + passwordStrengthWrapper.className = 'passwordStrengthWrapper'; + + var rating = elCreate('div'); + rating.className = 'passwordStrengthRating'; + + var ratingLabel = elCreate('small'); + ratingLabel.textContent = Language.get('wcf.user.password.strength'); + rating.appendChild(ratingLabel); + + this._score = elCreate('span'); + this._score.className = 'passwordStrengthScore'; + elData(this._score, 'score', '-1'); + rating.appendChild(this._score); + + this._wrapper.appendChild(rating); + + this._feedback = elCreate('div'); + this._feedback.className = 'passwordStrengthFeedback'; + this._wrapper.appendChild(this._feedback); + this._verdictResult = elCreate('input'); this._verdictResult.type = 'hidden'; this._verdictResult.name = this._input.name + '_passwordStrengthVerdict'; - this._input.parentNode.insertBefore(this._verdictResult, this._input); + this._wrapper.parentNode.insertBefore(this._verdictResult, this._wrapper); - this._input.addEventListener('input', this._evalute.bind(this)); + var callback = this._evaluate.bind(this); + this._input.addEventListener('input', callback); this._options.relatedInputs.forEach(function (input) { - input.addEventListener('input', this._evalute.bind(this)); - }.bind(this)); + input.addEventListener('input', callback); + }); - this._evalute(); + if (this._input.value.trim() !== '') { + this._evaluate(); + } }, - _evalute: function() { - var dictionary = flatMap(STATIC_DICTIONARY.concat( - this._options.staticDictionary, + /** + * @param {Event=} event + */ + _evaluate: function (event) { + var dictionary = flatMap(STATIC_DICTIONARY.concat(this._options.staticDictionary, this._options.relatedInputs.map(function (input) { - return input.value + return input.value.trim(); }) ), splitIntoWords).filter(function (value) { return value.length > 0; }); - + + var value = this._input.value.trim(); + // To bound runtime latency for really long passwords, consider sending zxcvbn() only // the first 100 characters or so of user input. - var verdict = zxcvbn(this._input.value.substr(0, 100), dictionary); + var verdict = zxcvbn(value.substr(0, 100), dictionary); verdict.feedback = this._options.feedbacker.from_result(verdict); - this._meter.value = verdict.score; + elData(this._score, 'score', value.length === 0 ? '-1' : verdict.score); + + if (event !== undefined) { + // Do not overwrite the value on page load. + elInnerError(this._wrapper, verdict.feedback.warning); + } + this._verdictResult.value = JSON.stringify(verdict); - }, + } }; return PasswordStrength; diff --git a/wcfsetup/install/files/style/layout/form.scss b/wcfsetup/install/files/style/layout/form.scss index 50d1f39181..492c6baacc 100644 --- a/wcfsetup/install/files/style/layout/form.scss +++ b/wcfsetup/install/files/style/layout/form.scss @@ -283,3 +283,66 @@ input { .customOptionRequired { color: rgba(204, 0, 1, 1) !important; } + +/* password strength estimator */ +.inputAddonPasswordStrength { + align-items: flex-start; + + @include screen-xs { + flex-direction: column; + } +} + +.passwordStrengthRating { + flex: 0 0 auto; + + @include screen-sm-up { + margin-left: 10px; + } + + @include screen-xs { + margin-top: 5px; + width: 100%; + } +} + +.passwordStrengthScore { + background-color: rgb(224, 224, 224); + border-radius: 3px; + display: block; + height: 10px; + overflow: hidden; + position: relative; + + &::before { + background-color: transparent; + bottom: 0; + content: ""; + left: 0; + position: absolute; + top: 0; + transition: background-color .12s linear, width .12s linear; + width: 0; + } + + &[data-score="0"]::before { + background-color: #DD2C00; + width: 20%; + } + &[data-score="1"]::before { + background-color: #FF9100; + width: 40%; + } + &[data-score="2"]::before { + background-color: #CDDC39; + width: 60%; + } + &[data-score="3"]::before { + background-color: #64DD17; + width: 80%; + } + &[data-score="4"]::before { + background-color: #2E7D32; + width: 100%; + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 15dcf75ccd..5689f28091 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4539,6 +4539,7 @@ Dateianhänge: + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index b80e16a01a..fa65c6ce35 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -4540,6 +4540,7 @@ Attachments: + -- 2.20.1