🐛 Fixed incorrect username being saved by Safari when signing up via invitation

refs https://github.com/TryGhost/Ghost/issues/9868
- ensure signup task is always initiated via form submit
  - adds `defaultClick` option to `<GhTaskButton>` that allows the click event to bubble
- adds `autocomplete` values to signup form fields that match up to the spec
  - "name/display-name": `name`
  - "email": `username email`
  - "password": `new-password` / `current-password` depending on context
- 🔥 no-longer-relevant hacks for Chrome autocomplete
  - this still doesn't fix Chrome remembering the incorrect username unfortunately. Chrome will always select the input previous to the password that has had actual user input as the "username"
- 🔥 unused `authenticate` task in signup controller
This commit is contained in:
Kevin Ansfield 2019-01-31 10:27:40 +00:00
parent b7e0614362
commit b3716505fa
7 changed files with 77 additions and 103 deletions

View File

@ -28,6 +28,7 @@ const GhTaskButton = Component.extend({
task: null,
disabled: false,
defaultClick: false,
buttonText: 'Save',
idleClass: '',
runningClass: '',
@ -99,6 +100,16 @@ const GhTaskButton = Component.extend({
},
click() {
// let the default click bubble if defaultClick===true - useful when
// you want to handle a form submit action rather than triggering a
// task directly
if (this.defaultClick) {
if (!this.isRunning) {
this.get('_restartAnimation').perform();
}
return;
}
// do nothing if disabled externally
if (this.get('disabled')) {
return false;

View File

@ -1,11 +1,9 @@
import Controller from '@ember/controller';
import RSVP from 'rsvp';
import {
VersionMismatchError,
isVersionMismatchError
} from 'ghost-admin/services/ajax';
import {alias} from '@ember/object/computed';
import {isArray as isEmberArray} from '@ember/array';
import {
isVersionMismatchError
} from 'ghost-admin/services/ajax';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
@ -29,53 +27,14 @@ export default Controller.extend({
setImage(image) {
this.set('profileImage', image);
},
submit(event) {
event.preventDefault();
this.signup.perform();
}
},
authenticate: task(function* (authStrategy, authentication) {
try {
let authResult = yield this.get('session')
.authenticate(authStrategy, ...authentication);
let promises = [];
promises.pushObject(this.get('settings').fetch());
promises.pushObject(this.get('config').fetchPrivate());
// fetch settings and private config for synchronous access
yield RSVP.all(promises);
return authResult;
} catch (error) {
if (error && error.payload && error.payload.errors) {
// we don't get back an ember-data/ember-ajax error object
// back so we need to pass in a null status in order to
// test against the payload
if (isVersionMismatchError(null, error)) {
let versionMismatchError = new VersionMismatchError(error);
return this.get('notifications').showAPIError(versionMismatchError);
}
error.payload.errors.forEach((err) => {
err.message = err.message.htmlSafe();
});
this.set('flowErrors', error.payload.errors[0].message.string);
if (error.payload.errors[0].message.string.match(/user with that email/)) {
this.get('signupDetails.errors').add('email', '');
}
if (error.payload.errors[0].message.string.match(/password is incorrect/)) {
this.get('signupDetails.errors').add('password', '');
}
} else {
// Connection errors don't return proper status message, only req.body
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
throw error;
}
}
}).drop(),
signup: task(function* () {
let setupProperties = ['name', 'email', 'password', 'token'];
let notifications = this.get('notifications');

View File

@ -3,10 +3,6 @@
</header>
<form id="setup" class="gh-flow-create">
{{!-- Horrible hack to prevent Chrome from incorrectly auto-filling inputs --}}
<input style="display:none;" type="text" name="username"/>
<input style="display:none;" type="password" name="password"/>
{{gh-profile-image email=email setImage=(action "setImage")}}
{{#gh-form-group errors=errors hasValidated=hasValidated property="blogTitle"}}
@ -38,6 +34,7 @@
name="name"
placeholder="Eg. John H. Watson"
autocorrect="off"
autocomplete="name"
value=(readonly name)
input=(action (mut name) value="target.value")
focus-out=(action "preValidate" "name")
@ -57,6 +54,7 @@
name="email"
placeholder="Eg. john@example.com"
autocorrect="off"
autocomplete="username email"
value=(readonly email)
input=(action (mut email) value="target.value")
focus-out=(action "preValidate" "email")
@ -76,6 +74,7 @@
name="password"
placeholder="At least 10 characters"
autocorrect="off"
autocomplete="new-password"
value=(readonly password)
input=(action (mut password) value="target.value")
focus-out=(action "preValidate" "password")

View File

@ -12,6 +12,7 @@
name="identification"
autocapitalize="off"
autocorrect="off"
autocomplete="username"
tabindex="1"
value=(readonly signin.identification)
input=(action (mut signin.identification) value="target.value")
@ -28,6 +29,7 @@
placeholder="Password"
name="password"
tabindex="2"
autocomplete="current-password"
autocorrect="off"
value=(readonly signin.password)
input=(action (mut signin.password) value="target.value")}}

View File

@ -6,11 +6,7 @@
<h1>Create your account</h1>
</header>
<form id="signup" class="gh-flow-create" method="post" novalidate="novalidate" {{action (perform submit) on="submit"}}>
{{!-- Hack to stop Chrome's broken auto-fills --}}
<input style="display:none;" type="text" name="fakeusernameremembered"/>
<input style="display:none;" type="password" name="fakepasswordremembered"/>
<form id="signup" class="gh-flow-create" method="post" novalidate="novalidate" onsubmit={{action "submit"}}>
{{gh-profile-image email=signupDetails.email setImage=(action "setImage")}}
{{#gh-form-group errors=signupDetails.errors hasValidated=signupDetails.hasValidated property="email"}}
@ -18,14 +14,17 @@
<span class="gh-input-icon gh-icon-mail">
{{svg-jar "email"}}
{{gh-text-input
type="email"
id="email"
name="email"
tabindex="2"
type="text"
id="username"
name="username"
placeholder="Eg. john@example.com"
disabled="disabled"
autocorrect="off"
autocomplete="username email"
disabled="disabled"
value=(readonly signupDetails.email)
input=(action (mut signupDetails.email) value="target.value")
data-test-input="email"
}}
</span>
{{/gh-form-group}}
@ -37,13 +36,15 @@
{{gh-trim-focus-input
tabindex="1"
type="text"
id="name"
name="name"
id="display-name"
name="display-name"
placeholder="Eg. John H. Watson"
autocorrect="off"
autocomplete="name"
value=(readonly signupDetails.name)
input=(action (mut signupDetails.name) value="target.value")
focus-out=(action "validate" "name")
data-test-input="name"
}}
</span>
{{gh-error-message errors=signupDetails.errors property="name"}}
@ -54,26 +55,27 @@
<span class="gh-input-icon gh-icon-lock">
{{svg-jar "lock"}}
{{gh-text-input
tabindex="2"
tabindex="3"
type="password"
id="password"
name="password"
placeholder="At least 10 characters"
autocorrect="off"
autocomplete="new-password"
value=(readonly signupDetails.password)
input=(action (mut signupDetails.password) value="target.value")
focus-out=(action "validate" "password")}}
focus-out=(action "validate" "password")
data-test-input="password"
}}
</span>
{{gh-error-message errors=signupDetails.errors property="password"}}
{{/gh-form-group}}
{{!-- include the email field again in case password managers ignore the disabled input --}}
<input type="hidden" name="email" value={{signupDetails.email}} />
</form>
{{gh-task-button "Create Account"
type="submit"
form="signup"
defaultClick=true
runningText="Creating"
task=signup
class="gh-btn gh-btn-green gh-btn-lg gh-btn-block gh-btn-icon"

View File

@ -117,10 +117,6 @@
{{/if}}
</figure>
{{!-- Horrible hack to prevent Chrome from incorrectly auto-filling inputs --}}
<input style="display:none;" type="text" name="fakeusernameremembered"/>
<input style="display:none;" type="password" name="fakepasswordremembered"/>
<fieldset class="user-details-bottom">
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="name" class="first-form-group"}}
@ -288,12 +284,13 @@
{{#if canChangePassword}}
<form id="password-reset" class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform user.saveNewPassword) on="submit"}}>
<fieldset>
{{#unless isNotOwnProfile}}
{{#if isOwnProfile}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="password"}}
<label for="user-password-old">Old Password</label>
{{gh-text-input
type="password"
id="user-password-old"
autocomplete="current-password"
value=(readonly user.password)
input=(action "updatePassword" value="target.value")
keyEvents=(hash
@ -303,13 +300,14 @@
}}
{{gh-error-message errors=user.errors property="password" data-test-error="user-old-pass"}}
{{/gh-form-group}}
{{/unless}}
{{/if}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="newPassword"}}
<label for="user-password-new">New Password</label>
{{gh-text-input
value=(readonly user.newPassword)
type="password"
autocomplete="new-password"
id="user-password-new"
input=(action "updateNewPassword" value="target.value")
keyEvents=(hash

View File

@ -45,111 +45,114 @@ describe('Acceptance: Signup', function () {
// email address should be pre-filled and disabled
expect(
find('input[name="email"]').value,
find('[data-test-input="email"]').value,
'email field value'
).to.equal('kevin+test2@ghost.org');
expect(
find('input[name="email"]').matches(':disabled'),
find('[data-test-input="email"]').matches(':disabled'),
'email field is disabled'
).to.be.true;
// focus out in Name field triggers inline error
await blur('input[name="name"]');
await blur('[data-test-input="name"]');
expect(
find('input[name="name"]').closest('.form-group'),
find('[data-test-input="name"]').closest('.form-group'),
'name field group has error class when empty'
).to.have.class('error');
expect(
find('input[name="name"]').closest('.form-group').querySelector('.response').textContent,
find('[data-test-input="name"]').closest('.form-group').querySelector('.response').textContent,
'name inline-error text'
).to.have.string('Please enter a name');
// entering text in Name field clears error
await fillIn('input[name="name"]', 'Test User');
await blur('input[name="name"]');
await fillIn('[data-test-input="name"]', 'Test User');
await blur('[data-test-input="name"]');
expect(
find('input[name="name"]').closest('.form-group'),
find('[data-test-input="name"]').closest('.form-group'),
'name field loses error class after text input'
).to.not.have.class('error');
expect(
find('input[name="name"]').closest('.form-group').querySelector('.response').textContent.trim(),
find('[data-test-input="name"]').closest('.form-group').querySelector('.response').textContent.trim(),
'name field error is removed after text input'
).to.be.empty;
// check password validation
// focus out in password field triggers inline error
// no password
await click('input[name="password"]');
await click('[data-test-input="password"]');
await blur();
expect(
find('input[name="password"]').closest('.form-group'),
find('[data-test-input="password"]').closest('.form-group'),
'password field group has error class when empty'
).to.have.class('error');
expect(
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
find('[data-test-input="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.have.string('must be at least 10 characters');
// password too short
await fillIn('input[name="password"]', 'short');
await blur('input[name="password"]');
await fillIn('[data-test-input="password"]', 'short');
await blur('[data-test-input="password"]');
expect(
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
find('[data-test-input="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.have.string('must be at least 10 characters');
// password must not be a bad password
await fillIn('input[name="password"]', '1234567890');
await blur('input[name="password"]');
await fillIn('[data-test-input="password"]', '1234567890');
await blur('[data-test-input="password"]');
expect(
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
find('[data-test-input="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.have.string('you cannot use an insecure password');
// password must not be a disallowed password
await fillIn('input[name="password"]', 'password99');
await blur('input[name="password"]');
await fillIn('[data-test-input="password"]', 'password99');
await blur('[data-test-input="password"]');
expect(
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
find('[data-test-input="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.have.string('you cannot use an insecure password');
// password must not have repeating characters
await fillIn('input[name="password"]', '2222222222');
await blur('input[name="password"]');
await fillIn('[data-test-input="password"]', '2222222222');
await blur('[data-test-input="password"]');
expect(
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
find('[data-test-input="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.have.string('you cannot use an insecure password');
// entering valid text in Password field clears error
await fillIn('input[name="password"]', 'thisissupersafe');
await blur('input[name="password"]');
await fillIn('[data-test-input="password"]', 'thisissupersafe');
await blur('[data-test-input="password"]');
expect(
find('input[name="password"]').closest('.form-group'),
find('[data-test-input="password"]').closest('.form-group'),
'password field loses error class after text input'
).to.not.have.class('error');
expect(
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent.trim(),
find('[data-test-input="password"]').closest('.form-group').querySelector('.response').textContent.trim(),
'password field error is removed after text input'
).to.equal('');
// submitting sends correct details and redirects to content screen
await click('.gh-btn-green');
this.timeout(0);
await this.pauseTest();
expect(currentRouteName()).to.equal('posts.index');
});