mirror of
https://github.com/discourse/discourse.git
synced 2025-03-30 04:45:36 +08:00
Login with email/forget password UI refactoring
* move button into login modal with social buttons * adds email link next to login field when filling it * adds proper validation messages * improves forgot password flash clearing * more tests
This commit is contained in:
parent
720e1965e3
commit
6f5acfe783
@ -14,11 +14,13 @@ export default Ember.Component.extend({
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, this._afterFirstRender);
|
||||
this.appEvents.on('modal-body:flash', msg => this._flash(msg));
|
||||
this.appEvents.on('modal-body:clearFlash', () => this._clearFlash());
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
this.appEvents.off('modal-body:flash');
|
||||
this.appEvents.off('modal-body:clearFlash');
|
||||
},
|
||||
|
||||
_afterFirstRender() {
|
||||
@ -45,10 +47,16 @@ export default Ember.Component.extend({
|
||||
);
|
||||
},
|
||||
|
||||
_clearFlash() {
|
||||
$('#modal-alert').hide().removeClass('alert-error', 'alert-success');
|
||||
},
|
||||
|
||||
_flash(msg) {
|
||||
$('#modal-alert').hide()
|
||||
.removeClass('alert-error', 'alert-success')
|
||||
.addClass(`alert alert-${msg.messageClass || 'success'}`).html(msg.text || '')
|
||||
.fadeIn();
|
||||
this._clearFlash();
|
||||
|
||||
$('#modal-alert')
|
||||
.addClass(`alert alert-${msg.messageClass || 'success'}`)
|
||||
.html(msg.text || '')
|
||||
.fadeIn();
|
||||
},
|
||||
});
|
||||
|
@ -13,8 +13,12 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
externalLogin: function(provider) {
|
||||
this.sendAction('action', provider);
|
||||
emailLogin() {
|
||||
this.sendAction('emailLogin');
|
||||
},
|
||||
|
||||
externalLogin(provider) {
|
||||
this.sendAction('externalLogin', provider);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -29,45 +29,36 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
},
|
||||
|
||||
resetPassword() {
|
||||
return this._submit('/session/forgot_password', 'forgot_password.complete');
|
||||
},
|
||||
if (this.get('submitDisabled')) return false;
|
||||
this.set('disabled', true);
|
||||
|
||||
emailLogin() {
|
||||
return this._submit('/u/email-login', 'email_login.complete');
|
||||
this.clearFlash();
|
||||
|
||||
ajax('/session/forgot_password', {
|
||||
data: { login: this.get('accountEmailOrUsername').trim() },
|
||||
type: 'POST'
|
||||
}).then(data => {
|
||||
const accountEmailOrUsername = escapeExpression(this.get("accountEmailOrUsername"));
|
||||
const isEmail = accountEmailOrUsername.match(/@/);
|
||||
let key = `forgot_password.complete_${isEmail ? 'email' : 'username'}`;
|
||||
if (data.user_found) {
|
||||
this.set('offerHelp', I18n.t(`${key}_found`, {
|
||||
email: accountEmailOrUsername,
|
||||
username: accountEmailOrUsername
|
||||
}));
|
||||
} else {
|
||||
this.flash(I18n.t(`${key}_not_found`, {
|
||||
email: accountEmailOrUsername,
|
||||
username: accountEmailOrUsername
|
||||
}), 'error');
|
||||
}
|
||||
}).catch(e => {
|
||||
this.flash(extractError(e), 'error');
|
||||
}).finally(() => {
|
||||
this.set('disabled', false);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_submit(route, translationKey) {
|
||||
if (this.get('submitDisabled')) return false;
|
||||
this.set('disabled', true);
|
||||
|
||||
ajax(route, {
|
||||
data: { login: this.get('accountEmailOrUsername').trim() },
|
||||
type: 'POST'
|
||||
}).then(data => {
|
||||
const escaped = escapeExpression(this.get('accountEmailOrUsername'));
|
||||
const isEmail = this.get('accountEmailOrUsername').match(/@/);
|
||||
let key = `${translationKey}_${isEmail ? 'email' : 'username'}`;
|
||||
let extraClass;
|
||||
|
||||
if (data.user_found === true) {
|
||||
key += '_found';
|
||||
this.set('accountEmailOrUsername', '');
|
||||
this.set('offerHelp', I18n.t(key, { email: escaped, username: escaped }));
|
||||
} else {
|
||||
if (data.user_found === false) {
|
||||
key += '_not_found';
|
||||
extraClass = 'error';
|
||||
}
|
||||
|
||||
this.flash(I18n.t(key, { email: escaped, username: escaped }), extraClass);
|
||||
}
|
||||
}).catch(e => {
|
||||
this.flash(extractError(e), 'error');
|
||||
}).finally(() => {
|
||||
this.set('disabled', false);
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
@ -4,6 +4,8 @@ import showModal from 'discourse/lib/show-modal';
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import { findAll } from 'discourse/models/login-method';
|
||||
import { escape } from 'pretty-text/sanitizer';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
import { extractError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
// This is happening outside of the app via popup
|
||||
@ -24,8 +26,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
authenticate: null,
|
||||
loggingIn: false,
|
||||
loggedIn: false,
|
||||
processingEmailLink: false,
|
||||
|
||||
canLoginLocal: setting('enable_local_logins'),
|
||||
canLoginLocalWithEmail: setting('enable_local_logins_via_email'),
|
||||
loginRequired: Em.computed.alias('application.loginRequired'),
|
||||
|
||||
resetForm: function() {
|
||||
@ -59,6 +63,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
return this.get('loggingIn') || this.get('authenticate');
|
||||
}.property('loggingIn', 'authenticate'),
|
||||
|
||||
@computed('canLoginLocalWithEmail', 'loginName', 'processingEmailLink')
|
||||
showLoginWithEmailLink(canLoginLocalWithEmail, loginName, processingEmailLink) {
|
||||
return canLoginLocalWithEmail && !Ember.isEmpty(loginName) && !processingEmailLink;
|
||||
},
|
||||
|
||||
actions: {
|
||||
login() {
|
||||
const self = this;
|
||||
@ -198,6 +207,37 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
const forgotPasswordController = this.get('forgotPassword');
|
||||
if (forgotPasswordController) { forgotPasswordController.set("accountEmailOrUsername", this.get("loginName")); }
|
||||
this.send("showForgotPassword");
|
||||
},
|
||||
|
||||
emailLogin() {
|
||||
if (this.get('processingEmailLink')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Ember.isEmpty(this.get('loginName'))){
|
||||
this.flash(I18n.t('login.blank_username'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('processingEmailLink', true);
|
||||
|
||||
ajax('/u/email-login', {
|
||||
data: { login: this.get('loginName').trim() },
|
||||
type: 'POST'
|
||||
}).then(data => {
|
||||
const loginName = escapeExpression(this.get('loginName'));
|
||||
const isEmail = loginName.match(/@/);
|
||||
let key = `email_login.complete_${isEmail ? 'email' : 'username'}`;
|
||||
if (data.user_found) {
|
||||
this.flash(I18n.t(`${key}_found`, { email: loginName, username: loginName }));
|
||||
} else {
|
||||
this.flash(I18n.t(`${key}_not_found`, { email: loginName, username: loginName }), 'error');
|
||||
}
|
||||
}).catch(e => {
|
||||
this.flash(extractError(e), 'error');
|
||||
}).finally(() => {
|
||||
this.set('processingEmailLink', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -5,6 +5,10 @@ export default Ember.Mixin.create({
|
||||
this.appEvents.trigger('modal-body:flash', { text, messageClass });
|
||||
},
|
||||
|
||||
clearFlash() {
|
||||
this.appEvents.trigger('modal-body:clearFlash');
|
||||
},
|
||||
|
||||
showModal(...args) {
|
||||
return showModal(...args);
|
||||
},
|
||||
|
@ -1,3 +1,12 @@
|
||||
{{#each buttons as |b|}}
|
||||
<button class="btn btn-social {{b.name}}" {{action "externalLogin" b}}>{{b.title}}</button>
|
||||
{{/each}}
|
||||
|
||||
{{#if canLoginLocalWithEmail}}
|
||||
{{d-button
|
||||
action="emailLogin"
|
||||
label="email_login.button_label"
|
||||
disabled=processingEmailLink
|
||||
icon="envelope-o"
|
||||
class="login-with-email-button"}}
|
||||
{{/if}}
|
||||
|
@ -1,6 +1,11 @@
|
||||
{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action="login"}}
|
||||
{{#d-modal-body title="login.title" class="login-modal"}}
|
||||
{{login-buttons action="externalLogin"}}
|
||||
{{login-buttons
|
||||
canLoginLocalWithEmail=canLoginLocalWithEmail
|
||||
processingEmailLink=processingEmailLink
|
||||
emailLogin='emailLogin'
|
||||
externalLogin='externalLogin'}}
|
||||
|
||||
{{#if canLoginLocal}}
|
||||
<form id='login-form' method='post'>
|
||||
<div id="credentials">
|
||||
@ -13,6 +18,16 @@
|
||||
{{text-field value=loginName type="email" placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off"}}
|
||||
</td>
|
||||
</tr>
|
||||
{{#if showLoginWithEmailLink}}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a class="login-with-email-link" {{action "emailLogin"}}>
|
||||
{{i18n 'email_login.link_label'}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
<tr>
|
||||
<td>
|
||||
<label for='login-account-password'>{{i18n 'login.password'}} </label>
|
||||
|
@ -13,12 +13,6 @@
|
||||
label="forgot_password.reset"
|
||||
disabled=submitDisabled
|
||||
class="btn-primary"}}
|
||||
{{#if siteSettings.enable_local_logins_via_email}}
|
||||
{{d-button action="emailLogin"
|
||||
label="email_login.label"
|
||||
disabled=submitDisabled
|
||||
class="email-login"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{d-button class="btn-large btn-primary"
|
||||
label="forgot_password.button_ok"
|
||||
|
@ -1,6 +1,11 @@
|
||||
{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action="login"}}
|
||||
{{#d-modal-body title="login.title" class="login-modal"}}
|
||||
{{login-buttons action="externalLogin"}}
|
||||
{{login-buttons
|
||||
canLoginLocalWithEmail=canLoginLocalWithEmail
|
||||
processingEmailLink=processingEmailLink
|
||||
emailLogin='emailLogin'
|
||||
externalLogin='externalLogin'}}
|
||||
|
||||
{{#if canLoginLocal}}
|
||||
<form id='login-form' method='post'>
|
||||
<div id="credentials">
|
||||
@ -8,7 +13,13 @@
|
||||
<tr>
|
||||
<td><label for='login-account-name'>{{i18n 'login.username'}}</label></td>
|
||||
<td>{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus"}}</td>
|
||||
<td></td>
|
||||
<td>
|
||||
{{#if showLoginWithEmailLink}}
|
||||
<a class="login-with-email-link" {{action "emailLogin"}}>
|
||||
{{i18n 'email_login.link_label'}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for='login-account-password'>{{i18n 'login.password'}}</label></td>
|
||||
|
@ -128,7 +128,7 @@
|
||||
&:before {
|
||||
margin-right: 9px;
|
||||
font-family: FontAwesome;
|
||||
font-size: 1.214em;
|
||||
font-size: $font-0;
|
||||
}
|
||||
&.google, &.google_oauth2 {
|
||||
background: $google;
|
||||
|
@ -1097,7 +1097,8 @@ en:
|
||||
button_help: "Help"
|
||||
|
||||
email_login:
|
||||
label: "Login With Email"
|
||||
link_label: "Email me a magic link"
|
||||
button_label: "with email"
|
||||
complete_username: "If an account matches the username <b>%{username}</b>, you should receive an email with a magic login link shortly."
|
||||
complete_email: "If an account matches <b>%{email}</b>, you should receive an email with a magic login link shortly."
|
||||
complete_username_found: "We found an account that matches the username <b>%{username}</b>, you should receive an email with a magic login link shortly."
|
||||
@ -1116,6 +1117,7 @@ en:
|
||||
caps_lock_warning: "Caps Lock is on"
|
||||
error: "Unknown error"
|
||||
rate_limit: "Please wait before trying to log in again."
|
||||
blank_username: "Please enter your email or username."
|
||||
blank_username_or_password: "Please enter your email or username, and password."
|
||||
reset_password: 'Reset Password'
|
||||
logging_in: "Signing In..."
|
||||
|
@ -3,9 +3,6 @@ import { acceptance } from "helpers/qunit-helpers";
|
||||
let userFound = false;
|
||||
|
||||
acceptance("Forgot password", {
|
||||
settings: {
|
||||
enable_local_logins_via_email: true
|
||||
},
|
||||
beforeEach() {
|
||||
const response = object => {
|
||||
return [
|
||||
@ -15,41 +12,44 @@ acceptance("Forgot password", {
|
||||
];
|
||||
};
|
||||
|
||||
server.post('/u/email-login', () => { // eslint-disable-line no-undef
|
||||
server.post('/session/forgot_password', () => { // eslint-disable-line no-undef
|
||||
return response({ "user_found": userFound });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("logging in via email", assert => {
|
||||
QUnit.test("requesting password reset", assert => {
|
||||
visit("/");
|
||||
click("header .login-button");
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(exists('.login-modal'), "it shows the login modal");
|
||||
});
|
||||
|
||||
click('#forgot-password-link');
|
||||
|
||||
fillIn("#username-or-email", 'someuser');
|
||||
click('.email-login');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-error").html(),
|
||||
I18n.t('email_login.complete_username_not_found', { username: 'someuser' }),
|
||||
'it should display the right error message'
|
||||
find('button[title="Reset Password"]').attr("disabled"),
|
||||
"disabled",
|
||||
'it should disable the button until the field is filled'
|
||||
);
|
||||
});
|
||||
|
||||
fillIn("#username-or-email", 'someuser');
|
||||
click('button[title="Reset Password"]');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-error").html().trim(),
|
||||
I18n.t('forgot_password.complete_username_not_found', { username: 'someuser' }),
|
||||
'it should display an error for an invalid username'
|
||||
);
|
||||
});
|
||||
|
||||
fillIn("#username-or-email", 'someuser@gmail.com');
|
||||
click('.email-login');
|
||||
click('button[title="Reset Password"]');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-error").html(),
|
||||
I18n.t('email_login.complete_email_not_found', { email: 'someuser@gmail.com' }),
|
||||
'it should display the right error message'
|
||||
find(".alert-error").html().trim(),
|
||||
I18n.t('forgot_password.complete_email_not_found', { email: 'someuser@gmail.com' }),
|
||||
'it should display an error for an invalid email'
|
||||
);
|
||||
});
|
||||
|
||||
@ -59,32 +59,29 @@ QUnit.test("logging in via email", assert => {
|
||||
userFound = true;
|
||||
});
|
||||
|
||||
click('.email-login');
|
||||
click('button[title="Reset Password"]');
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(exists(find(".alert-error")), 'it should remove the flash error when succeeding');
|
||||
|
||||
assert.equal(
|
||||
find(".modal-body").html().trim(),
|
||||
I18n.t('email_login.complete_username_found', { username: 'someuser' }),
|
||||
'it should display the right message'
|
||||
I18n.t('forgot_password.complete_username_found', { username: 'someuser' }),
|
||||
'it should display a success message for a valid username'
|
||||
);
|
||||
});
|
||||
|
||||
visit("/");
|
||||
click("header .login-button");
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(exists('.login-modal'), "it shows the login modal");
|
||||
});
|
||||
|
||||
click('#forgot-password-link');
|
||||
fillIn("#username-or-email", 'someuser@gmail.com');
|
||||
click('.email-login');
|
||||
click('button[title="Reset Password"]');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".modal-body").html().trim(),
|
||||
I18n.t('email_login.complete_email_found', { email: 'someuser@gmail.com' }),
|
||||
'it should display the right message'
|
||||
I18n.t('forgot_password.complete_email_found', { email: 'someuser@gmail.com' }),
|
||||
'it should display a success message for a valid email'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
116
test/javascripts/acceptance/login-with-email-test.js.es6
Normal file
116
test/javascripts/acceptance/login-with-email-test.js.es6
Normal file
@ -0,0 +1,116 @@
|
||||
import { acceptance } from "helpers/qunit-helpers";
|
||||
|
||||
let userFound = false;
|
||||
|
||||
acceptance("Login with email", {
|
||||
settings: {
|
||||
enable_local_logins_via_email: true,
|
||||
enable_facebook_logins: true
|
||||
},
|
||||
beforeEach() {
|
||||
const response = object => {
|
||||
return [
|
||||
200,
|
||||
{ "Content-Type": "application/json" },
|
||||
object
|
||||
];
|
||||
};
|
||||
|
||||
server.post('/u/email-login', () => { // eslint-disable-line no-undef
|
||||
return response({ "user_found": userFound });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("logging in via email (link)", assert => {
|
||||
visit("/");
|
||||
click("header .login-button");
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(exists(".login-with-email-link"), 'it displays the link only when field is filled');
|
||||
});
|
||||
|
||||
fillIn("#login-account-name", "someuser");
|
||||
click(".login-with-email-link");
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-error").html(),
|
||||
I18n.t('email_login.complete_username_not_found', { username: 'someuser' }),
|
||||
'it should display an error for an invalid username'
|
||||
);
|
||||
});
|
||||
|
||||
fillIn("#login-account-name", 'someuser@gmail.com');
|
||||
click('.login-with-email-link');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-error").html(),
|
||||
I18n.t('email_login.complete_email_not_found', { email: 'someuser@gmail.com' }),
|
||||
'it should display an error for an invalid email'
|
||||
);
|
||||
});
|
||||
|
||||
fillIn("#login-account-name", 'someuser');
|
||||
|
||||
andThen(() => {
|
||||
userFound = true;
|
||||
});
|
||||
|
||||
click('.login-with-email-link');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-success").html().trim(),
|
||||
I18n.t('email_login.complete_username_found', { username: 'someuser' }),
|
||||
'it should display a success message for a valid username'
|
||||
);
|
||||
});
|
||||
|
||||
visit("/");
|
||||
click("header .login-button");
|
||||
fillIn("#login-account-name", 'someuser@gmail.com');
|
||||
click('.login-with-email-link');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-success").html().trim(),
|
||||
I18n.t('email_login.complete_email_found', { email: 'someuser@gmail.com' }),
|
||||
'it should display a success message for a valid email'
|
||||
);
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
userFound = false;
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("logging in via email (button)", assert => {
|
||||
visit("/");
|
||||
click("header .login-button");
|
||||
click('.login-with-email-button');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-error").html(),
|
||||
I18n.t('login.blank_username'),
|
||||
'it should display an error for blank username'
|
||||
);
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
userFound = true;
|
||||
});
|
||||
|
||||
fillIn("#login-account-name", 'someuser');
|
||||
click('.login-with-email-button');
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find(".alert-success").html().trim(),
|
||||
I18n.t('email_login.complete_username_found', { username: 'someuser' }),
|
||||
'it should display a success message for a valid username'
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user