🐛 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:
parent
b7e0614362
commit
b3716505fa
@ -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;
|
||||
|
@ -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');
|
||||
|
@ -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")
|
||||
|
@ -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")}}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user