Skip to content

Added password strength gauge in registration form. #2542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Gulpfile.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ gulp.task("dist:images", () => {
.pipe(gulp.dest(path.join(distPath, "images")));
});

gulp.task("dist:vendor", () => {
return gulp.src(path.join(staticPrefix, "js", "vendor", "**", "*"))
.pipe(gulp.dest(path.join(distPath, "js", "vendor")));
});


gulp.task("dist:manifest", () => {
let paths = [
Expand Down Expand Up @@ -258,7 +263,7 @@ gulp.task("dist", (cb) => {
// any previously built files.
"clean",
// Build all of our static assets.
["dist:font-awesome", "dist:css", "dist:js"],
["dist:font-awesome", "dist:css", "dist:js", "dist:vendor"],
// We have this here, instead of in the list above even though there is no
// ordering dependency so that all of it's output shows up together which
// makes it easier to read.
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ lint: .state/env/pyvenv.cfg
$(BINDIR)/html_lint.py --printfilename --disable=optional_tag,names,protocol,extra_whitespace `find ./warehouse/templates -path ./warehouse/templates/legacy -prune -o -name '*.html' -print`
ifneq ($(TRAVIS), false)
# We're on Travis, so we can lint static files locally
./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js'
./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js' --ignore-pattern 'warehouse/static/js/vendor/**'
./node_modules/.bin/sass-lint --verbose
else
# We're not on Travis, so we should lint static files inside the static container
docker-compose run --rm static ./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js'
docker-compose run --rm static ./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js' --ignore-pattern 'warehouse/static/js/vendor/**'
docker-compose run --rm static ./node_modules/.bin/sass-lint --verbose
endif

Expand Down
28 changes: 28 additions & 0 deletions warehouse/static/js/vendor/zxcvbn.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions warehouse/static/js/warehouse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ docReady(Analytics);

// Handle the JS based automatic form submission.
docReady(formUtils.submitTriggers);
docReady(formUtils.registerFormValidation);

docReady(Statuspage);

Expand Down
93 changes: 93 additions & 0 deletions warehouse/static/js/warehouse/utils/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,96 @@ export function submitTriggers() {
);
}
}

/* global zxcvbn */

const tooltipClasses = ["tooltipped", "tooltipped-s", "tooltipped-immediate"];

const passwordStrengthValidator = (value) => {
const zxcvbnResult = zxcvbn(value);
return zxcvbnResult.score < 2 ?
zxcvbnResult.feedback.suggestions.join(" ") : null;
};

const fieldRequiredValidator = (value) => {
return value === ""?
"Please fill out this field" : null;
};

const checkPasswordStrength = (event) => {
let result = document.querySelector(".password-strength__gauge");
if (event.target.value === "") {
result.setAttribute("class", "password-strength__gauge");
// Feedback for screen readers
result.querySelector(".sr-only").innerHTML = "Password field is empty";
} else {
// following recommendations on the zxcvbn JS docs
// the zxcvbn function is available by loading `vendor/zxcvbn.js`
// in the register page template only
let zxcvbnResult = zxcvbn(event.target.value);
result.setAttribute("class", `password-strength__gauge password-strength__gauge--${zxcvbnResult.score}`);

// Feedback for screen readers
result.querySelector(".sr-only").innerHTML = zxcvbnResult.feedback.suggestions.join(" ") || "Password is strong";
}
};

const setupPasswordStrengthGauge = () => {
let password = document.querySelector("#new_password");
if (password === null) return;
password.addEventListener(
"input",
checkPasswordStrength,
false
);
};

const attachTooltip = (field, message) => {
let parentNode = field.parentNode;
parentNode.classList.add.apply(parentNode.classList, tooltipClasses);
parentNode.setAttribute("aria-label", message);
};

const removeTooltips = () => {
let tooltippedNodes = document.querySelectorAll(".tooltipped");
for (let tooltippedNode of tooltippedNodes) {
tooltippedNode.classList.remove.apply(tooltippedNode.classList, tooltipClasses);
tooltippedNode.removeAttribute("aria-label");
}
};

const validateForm = (event) => {
removeTooltips();
let inputFields = document.querySelectorAll("input[required='required']");
for (let inputField of inputFields) {
let requiredMessage = fieldRequiredValidator(inputField.value);
if (requiredMessage !== null) {
attachTooltip(inputField, requiredMessage);
event.preventDefault();
return false;
}
}

let password = document.querySelector("#new_password");
let passwordConfirm = document.querySelector("#password_confirm");
if (password.value !== passwordConfirm.value) {
let message = "Passwords do not match";
attachTooltip(password, message);
event.preventDefault();
return false;
}

let passwordStrengthMessage = passwordStrengthValidator(password.value);
if (passwordStrengthMessage !== null) {
attachTooltip(password, passwordStrengthMessage);
event.preventDefault();
return false;
}
};

export function registerFormValidation() {
if (document.querySelector("#password_confirm") === null) return;
setupPasswordStrengthGauge();
const submitButton = document.querySelector("#content input[type='submit']");
submitButton.addEventListener("click", validateForm, false);
}
59 changes: 59 additions & 0 deletions warehouse/static/sass/blocks/_password-strength.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*!
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/*
A password strength indicator:

<span class="password-strength">
<span class="password-strength__gauge"></span>
</span>
*/

.password-strength {
display: inline-block;
width: 100%;
height: 0.8em;
border: 1px solid $border-color;

.password-strength__gauge {
width: 0%;
height: 100%;
display: block;

&--0 {
width: 20%;
background-color: red;
}

&--1 {
width: 40%;
background-color: orange;
}

&--2 {
width: 60%;
background-color: yellow;
}

&--3 {
width: 80%;
background-color: #508000;
}

&--4 {
width: 100%;
background-color: green;
}
}
}
3 changes: 2 additions & 1 deletion warehouse/static/sass/blocks/_tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ $tooltip-duration: 0.1s !default;
// This will indicate when we'll activate the tooltip
.tooltipped:hover,
.tooltipped:active,
.tooltipped:focus {
.tooltipped:focus,
.tooltipped-immediate {
&::before,
&::after {
display: inline-block;
Expand Down
1 change: 1 addition & 0 deletions warehouse/static/sass/warehouse.scss
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
@import "blocks/package-description";
@import "blocks/package-header";
@import "blocks/package-snippet";
@import "blocks/password-strength";
@import "blocks/project-description";
@import "blocks/release";
@import "blocks/release-timeline";
Expand Down
19 changes: 16 additions & 3 deletions warehouse/templates/accounts/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ <h1 class="page-title">Create an account on PyPI</h1>

<div class="form-group">
<label for="full_name" class="form-group__label">Name</label>
{{ form.full_name(placeholder="Your name", class_="form-group__input") }}
{{ form.full_name(placeholder="Your name", required="required", class_="form-group__input") }}
{% if form.full_name.errors %}
<ul class="form-errors">
{% for error in form.full_name.errors %}
Expand Down Expand Up @@ -77,10 +77,20 @@ <h1 class="page-title">Create an account on PyPI</h1>
<input id="show-password" type="checkbox">&nbsp;Show passwords
</label>
</div>
{{ form.new_password(placeholder="Select a password", required="required", class_="form-group__input", autocomplete="new-password") }}
<div>
{{ form.new_password(placeholder="Select a password", required="required", class_="form-group__input", autocomplete="new-password") }}
</div>
<p class="form-group__help-text">
Choose a strong password that contains letters (uppercase and lowercase), numbers and special characters. Avoid common words or repetition.
</p>
<p class="form-group__help-text">
<strong>Password strength:</strong>
<span class="password-strength">
<span class="password-strength__gauge">
<span class="sr-only">Password field is empty</span>
</span>
</span>
</p>
{% if form.new_password.errors %}
<ul class="form-errors">
{% for error in form.new_password.errors %}
Expand All @@ -91,7 +101,7 @@ <h1 class="page-title">Create an account on PyPI</h1>
</div>

<div class="form-group">
<label for="password_confirm" class="sr-only">Confirm Password</label>
<label for="password_confirm" class="form-group__label">Confirm Password</label>
{{ form.password_confirm(placeholder="Confirm password", required="required", class_="form-group__input") }}
{% if form.password_confirm.errors %}
<ul class="form-errors">
Expand All @@ -114,4 +124,7 @@ <h1 class="page-title">Create an account on PyPI</h1>

{% block extra_js %}
{{ recaptcha_src(request) }}
<script async
src="{{ request.static_path('warehouse:static/dist/js/vendor/zxcvbn.js') }}">
</script>
{% endblock %}