diff --git a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 index 4311368186c..7e385fb3022 100644 --- a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 +++ b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 @@ -20,48 +20,54 @@ export default Ember.Controller.extend(ModalFunctionality, { }, actions: { - submit() { - if (this.get('submitDisabled')) return false; - - this.set('disabled', true); - - ajax('/session/forgot_password', { - data: { login: this.get('accountEmailOrUsername').trim() }, - type: 'POST' - }).then(data => { - const escaped = escapeExpression(this.get('accountEmailOrUsername')); - const isEmail = this.get('accountEmailOrUsername').match(/@/); - let key = 'forgot_password.complete_' + (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(() => { - setTimeout(() => this.set('disabled', false), 1000); - }); - - return false; - }, - ok() { this.send('closeModal'); }, help() { this.setProperties({ offerHelp: I18n.t('forgot_password.help'), helpSeen: true }); - } - } + }, + resetPassword() { + return this._submit('/session/forgot_password', 'forgot_password.complete'); + }, + + emailLogin() { + return this._submit('/u/email-login', 'email_login.complete'); + } + }, + + _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; + }, }); diff --git a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs index c739f771979..8b42f88739d 100644 --- a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs +++ b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs @@ -9,10 +9,16 @@ {{/d-modal-body}}
Not sure which email address you used? Enter an email address and we’ll let you know if it exists here.
If you no longer have access to the email address on your account, please contact our helpful staff.
" button_ok: "OK" button_help: "Help" + + email_login: + label: "Login With Email" + complete_username: "If an account matches the username %{username}, you should receive an email with a magic login link shortly." + complete_email: "If an account matches %{email}, you should receive an email with a magic login link shortly." + complete_username_found: "We found an account that matches the username %{username}, you should receive an email with a magic login link shortly." + complete_email_found: "We found an account that matches %{email}, you should receive an email with a magic login link shortly." + complete_username_not_found: "No account matches the username %{username}" + complete_email_not_found: "No account matches %{email}" + login: title: "Log In" username: "User" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4e2b817c0f8..d5ef33a877f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -648,6 +648,10 @@ en: success: "You successfully changed your password and are now logged in." success_unapproved: "You successfully changed your password." + email_login: + invalid_token: "Sorry, that email login link is too old. Select the Log In button and use 'I forgot my password' to get a new link." + title: "Email login" + change_email: confirmed: "Your email has been updated." please_continue: "Continue to %{site_name}" @@ -1149,6 +1153,7 @@ en: sso_allows_all_return_paths: "Do not restrict the domain for return_paths provided by SSO (by default return path must be on current site)" enable_local_logins: "Enable local username and password login based accounts. (Note: this must be enabled for invites to work)" + enable_local_logins_via_email: "Email user logins via email." allow_new_registrations: "Allow new user registrations. Uncheck this to prevent anyone from creating a new account." enable_signup_cta: "Show a notice to returning anonymous users prompting them to sign up for an account." enable_yahoo_logins: "Enable Yahoo authentication" @@ -1640,6 +1645,7 @@ en: staged_users_disabled: "You must first enable 'staged users' before enabling this setting." reply_by_email_disabled: "You must first enable 'reply by email' before enabling this setting." sso_url_is_empty: "You must set a 'sso url' before enabling this setting." + enable_local_logins_disabled: "You must first enable 'enable local logins' before enabling this setting." search: within_post: "#%{post_number} by %{username}" @@ -2772,6 +2778,17 @@ en: Click the following link to choose a new password: %{base_url}/u/password-reset/%{email_token} + email_login: + title: "Email login link" + subject_template: "[%{email_prefix}] Email login link" + text_body_template: | + Somebody asked to login your account on [%{site_name}](%{base_url}). + + If it was not you, you can safely ignore this email. + + Click the following link to login: + %{base_url}/session/email-login/%{email_token} + set_password: title: "Set Password" subject_template: "[%{email_prefix}] Set Password" diff --git a/config/routes.rb b/config/routes.rb index 0f51c44a175..11edecf0f9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -301,6 +301,7 @@ Discourse::Application.routes.draw do get "session/sso_provider" => "session#sso_provider" get "session/current" => "session#current" get "session/csrf" => "session#csrf" + get "session/email-login/:token" => "session#email_login" get "composer_messages" => "composer_messages#index" post "composer/parse_html" => "composer#parse_html" @@ -330,6 +331,7 @@ Discourse::Application.routes.draw do put "#{root_path}/update-activation-email" => "users#update_activation_email" get "#{root_path}/hp" => "users#get_honeypot_value" + post "#{root_path}/email-login" => "users#email_login" get "#{root_path}/admin-login" => "users#admin_login" put "#{root_path}/admin-login" => "users#admin_login" get "#{root_path}/admin-login/:token" => "users#admin_login" diff --git a/config/site_settings.yml b/config/site_settings.yml index 64b71a2b344..55163cd323b 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -240,6 +240,10 @@ login: enable_local_logins: client: true default: true + enable_local_logins_via_email: + client: true + default: false + validator: "EnableLocalLoginsViaEmailValidator" allow_new_registrations: client: true default: true @@ -331,7 +335,6 @@ login: default: 1440 min: 1 max: 175200 - users: min_username_length: client: true diff --git a/lib/validators/enable_local_logins_via_email_validator.rb b/lib/validators/enable_local_logins_via_email_validator.rb new file mode 100644 index 00000000000..0537f3697e7 --- /dev/null +++ b/lib/validators/enable_local_logins_via_email_validator.rb @@ -0,0 +1,14 @@ +class EnableLocalLoginsViaEmailValidator + def initialize(opts = {}) + @opts = opts + end + + def valid_value?(val) + return true if val == 'f' + SiteSetting.enable_local_logins + end + + def error_message + I18n.t('site_settings.errors.enable_local_logins_disabled') + end +end diff --git a/spec/components/validators/enable_local_logins_via_email_validator_spec.rb b/spec/components/validators/enable_local_logins_via_email_validator_spec.rb new file mode 100644 index 00000000000..289ce4911d0 --- /dev/null +++ b/spec/components/validators/enable_local_logins_via_email_validator_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +RSpec.describe EnableLocalLoginsViaEmailValidator do + subject { described_class.new } + + describe '#valid_value?' do + describe "when 'enable_local_logins' is false" do + before do + SiteSetting.enable_local_logins = false + end + + describe 'when val is false' do + it 'should be valid' do + expect(subject.valid_value?('f')).to eq(true) + end + end + + describe 'when value is true' do + it 'should not be valid' do + expect(subject.valid_value?('t')).to eq(false) + + expect(subject.error_message).to eq(I18n.t( + 'site_settings.errors.enable_local_logins_disabled' + )) + end + end + end + + describe "when 'enable_local_logins' is true" do + before do + SiteSetting.enable_local_logins = true + end + + describe 'when val is false' do + it 'should be valid' do + expect(subject.valid_value?('f')).to eq(true) + end + end + + describe 'when value is true' do + it 'should be valid' do + expect(subject.valid_value?('t')).to eq(true) + end + end + end + end +end diff --git a/spec/controllers/session_controller_spec.rb b/spec/controllers/session_controller_spec.rb index cc535e72223..96b58c5483d 100644 --- a/spec/controllers/session_controller_spec.rb +++ b/spec/controllers/session_controller_spec.rb @@ -8,7 +8,7 @@ describe SessionController do end end - describe 'become' do + describe '#become' do let!(:user) { Fabricate(:user) } it "does not work when not in development mode" do @@ -26,7 +26,7 @@ describe SessionController do end end - describe '.sso_login' do + describe '#sso_login' do before do @sso_url = "http://somesite.com/discourse_sso" @@ -410,7 +410,7 @@ describe SessionController do end end - describe '.sso_provider' do + describe '#sso_provider' do before do SiteSetting.enable_sso_provider = true SiteSetting.enable_sso = false @@ -470,7 +470,7 @@ describe SessionController do end end - describe '.create' do + describe '#create' do let(:user) { Fabricate(:user) } @@ -515,7 +515,9 @@ describe SessionController do login: user.username, password: 'sssss' }, format: :json - expect(::JSON.parse(response.body)['error']).to be_present + expect(::JSON.parse(response.body)['error']).to eq( + I18n.t("login.incorrect_username_email_or_password") + ) end end @@ -526,7 +528,9 @@ describe SessionController do login: user.username, password: ('s' * (User.max_password_length + 1)) }, format: :json - expect(::JSON.parse(response.body)['error']).to be_present + expect(::JSON.parse(response.body)['error']).to eq( + I18n.t("login.incorrect_username_email_or_password") + ) end end @@ -536,14 +540,15 @@ describe SessionController do user.suspended_at = Time.now user.save! StaffActionLogger.new(user).log_user_suspend(user, "