mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 14:53:39 +08:00
FEATURE: Let users update their emails before confirming
This allows users who entered a typo or invalid email address when signing up an opportunity to fix it and resending the confirmation email to that address.
This commit is contained in:
parent
d1c79372d7
commit
40ab2e5667
|
@ -25,6 +25,7 @@
|
|||
//= require ./discourse/lib/computed
|
||||
//= require ./discourse/lib/formatter
|
||||
//= require ./discourse/lib/eyeline
|
||||
//= require ./discourse/lib/show-modal
|
||||
//= require ./discourse/mixins/scrolling
|
||||
//= require ./discourse/models/model
|
||||
//= require ./discourse/models/rest
|
||||
|
@ -68,7 +69,6 @@
|
|||
//= require ./discourse/lib/emoji/groups
|
||||
//= require ./discourse/lib/emoji/toolbar
|
||||
//= require ./discourse/components/d-editor
|
||||
//= require ./discourse/lib/show-modal
|
||||
//= require ./discourse/lib/screen-track
|
||||
//= require ./discourse/routes/discourse
|
||||
//= require ./discourse/routes/build-topic-route
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { extractError } from 'discourse/lib/ajax-error';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
login: Ember.inject.controller(),
|
||||
|
||||
currentEmail: null,
|
||||
newEmail: null,
|
||||
password: null,
|
||||
|
||||
@computed('newEmail', 'currentEmail')
|
||||
submitDisabled(newEmail, currentEmail) {
|
||||
return newEmail === currentEmail;
|
||||
},
|
||||
|
||||
actions: {
|
||||
changeEmail() {
|
||||
const login = this.get('login');
|
||||
|
||||
ajax(userPath('update-activation-email'), {
|
||||
data: {
|
||||
username: login.get('loginName'),
|
||||
password: login.get('loginPassword'),
|
||||
email: this.get('newEmail')
|
||||
},
|
||||
type: 'PUT'
|
||||
}).then(() => {
|
||||
const modal = this.showModal('activation-resent', {title: 'log_in'});
|
||||
modal.set('currentEmail', this.get('newEmail'));
|
||||
}).catch(err => this.flash(extractError(err), 'error'));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
modal: null
|
||||
});
|
|
@ -4,21 +4,23 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
|||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
emailSent: false,
|
||||
|
||||
onShow() {
|
||||
this.set("emailSent", false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
sendActivationEmail() {
|
||||
ajax(userPath('action/send_activation_email'), {
|
||||
data: { username: this.get('username') },
|
||||
type: 'POST'
|
||||
}).then(() => {
|
||||
this.set('emailSent', true);
|
||||
const modal = this.showModal('activation-resent', {title: 'log_in'});
|
||||
modal.set('currentEmail', this.get('currentEmail'));
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
editActivationEmail() {
|
||||
const modal = this.showModal('activation-edit', {title: 'login.change_email'});
|
||||
|
||||
const currentEmail = this.get('currentEmail');
|
||||
modal.set('currentEmail', currentEmail);
|
||||
modal.set('newEmail', currentEmail);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -11,17 +11,23 @@ export default function(name, opts) {
|
|||
|
||||
const controllerName = opts.admin ? `modals/${name}` : name;
|
||||
|
||||
const controller = container.lookup('controller:' + controllerName);
|
||||
let controller = container.lookup('controller:' + controllerName);
|
||||
const templateName = opts.templateName || Ember.String.dasherize(name);
|
||||
|
||||
const renderArgs = { into: 'modal', outlet: 'modalBody'};
|
||||
if (controller) { renderArgs.controller = controllerName; }
|
||||
if (controller) {
|
||||
renderArgs.controller = controllerName;
|
||||
} else {
|
||||
// use a basic controller
|
||||
renderArgs.controller = 'basic-modal-body';
|
||||
controller = container.lookup(`controller:${renderArgs.controller}`);
|
||||
}
|
||||
|
||||
|
||||
if (opts.addModalBodyView) {
|
||||
renderArgs.view = 'modal-body';
|
||||
}
|
||||
|
||||
|
||||
const modalName = `modal/${templateName}`;
|
||||
const fullName = opts.admin ? `admin/templates/${modalName}` : modalName;
|
||||
route.render(fullName, renderArgs);
|
||||
|
@ -29,13 +35,11 @@ export default function(name, opts) {
|
|||
modalController.set('title', I18n.t(opts.title));
|
||||
}
|
||||
|
||||
if (controller) {
|
||||
controller.set('modal', modalController);
|
||||
const model = opts.model;
|
||||
if (model) { controller.set('model', model); }
|
||||
if (controller.onShow) { controller.onShow(); }
|
||||
controller.set('flashMessage', null);
|
||||
}
|
||||
controller.set('modal', modalController);
|
||||
const model = opts.model;
|
||||
if (model) { controller.set('model', model); }
|
||||
if (controller.onShow) { controller.onShow(); }
|
||||
controller.set('flashMessage', null);
|
||||
|
||||
return controller;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
flash(text, messageClass) {
|
||||
this.appEvents.trigger('modal-body:flash', { text, messageClass });
|
||||
},
|
||||
|
||||
showModal(...args) {
|
||||
return showModal(...args);
|
||||
},
|
||||
|
||||
actions: {
|
||||
closeModal() {
|
||||
this.get('modal').send('closeModal');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action closeModal}}>{{i18n 'close'}}</button>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
{{#d-modal-body}}
|
||||
<p>{{i18n "login.provide_new_email"}}</p>
|
||||
{{input value=newEmail class="activate-new-email"}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action="changeEmail"
|
||||
label="login.submit_new_email"
|
||||
disabled=submitDisabled
|
||||
class="btn-primary"}}
|
||||
{{d-button action="closeModal" label="close"}}
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
{{#d-modal-body}}
|
||||
{{{i18n 'login.sent_activation_email_again' currentEmail=currentEmail}}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
{{modal-footer-close closeModal=(action "closeModal")}}
|
|
@ -1,12 +1,14 @@
|
|||
{{#d-modal-body}}
|
||||
{{#if emailSent}}
|
||||
{{{i18n 'login.sent_activation_email_again' currentEmail=currentEmail}}}
|
||||
{{else}}
|
||||
{{{i18n 'login.not_activated' sentTo=sentTo}}}
|
||||
<a href {{action "sendActivationEmail"}} class="resend-link">{{i18n 'login.resend_activation_email'}}</a>
|
||||
{{/if}}
|
||||
{{{i18n 'login.not_activated' sentTo=sentTo}}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
|
||||
{{d-button action="sendActivationEmail"
|
||||
label="login.resend_title"
|
||||
icon="envelope"
|
||||
class="btn-primary resend"}}
|
||||
{{d-button action="editActivationEmail"
|
||||
label="login.change_email"
|
||||
icon="pencil"
|
||||
class="edit-email"}}
|
||||
</div>
|
||||
|
|
|
@ -376,3 +376,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-button-bar {
|
||||
margin-top: 1em;
|
||||
|
||||
button {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ class UsersController < ApplicationController
|
|||
:activate_account,
|
||||
:perform_account_activation,
|
||||
:send_activation_email,
|
||||
:update_activation_email,
|
||||
:password_reset,
|
||||
:confirm_email_token,
|
||||
:admin_login,
|
||||
|
@ -569,6 +570,28 @@ class UsersController < ApplicationController
|
|||
render layout: 'no_ember'
|
||||
end
|
||||
|
||||
def update_activation_email
|
||||
RateLimiter.new(nil, "activate-edit-email-hr-#{request.remote_ip}", 5, 1.hour).performed!
|
||||
|
||||
@user = User.find_by_username_or_email(params[:username])
|
||||
raise Discourse::InvalidAccess.new unless @user.present?
|
||||
raise Discourse::InvalidAccess.new if @user.active?
|
||||
raise Discourse::InvalidAccess.new if current_user.present?
|
||||
|
||||
raise Discourse::InvalidAccess.new unless @user.confirm_password?(params[:password])
|
||||
|
||||
User.transaction do
|
||||
@user.email = params[:email]
|
||||
if @user.save
|
||||
@user.email_tokens.create(email: @user.email)
|
||||
enqueue_activation_email
|
||||
render json: success_json
|
||||
else
|
||||
render_json_error(@user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def send_activation_email
|
||||
if current_user.blank? || !current_user.staff?
|
||||
RateLimiter.new(nil, "activate-hr-#{request.remote_ip}", 30, 1.hour).performed!
|
||||
|
|
|
@ -1040,6 +1040,12 @@ en:
|
|||
not_allowed_from_ip_address: "You can't login from that IP address."
|
||||
admin_not_allowed_from_ip_address: "You can't log in as admin from that IP address."
|
||||
resend_activation_email: "Click here to send the activation email again."
|
||||
|
||||
resend_title: "Resend Activation Email"
|
||||
change_email: "Change Email Address"
|
||||
provide_new_email: "Provide a new address and we'll resend your confirmation email."
|
||||
submit_new_email: "Update Email Address"
|
||||
|
||||
sent_activation_email_again: "We sent another activation email to you at <b>{{currentEmail}}</b>. It might take a few minutes for it to arrive; be sure to check your spam folder."
|
||||
to_continue: "Please Log In"
|
||||
preferences: "You need to be logged in to change your user preferences."
|
||||
|
|
|
@ -1655,7 +1655,7 @@ en:
|
|||
incorrect_username_email_or_password: "Incorrect username, email or password"
|
||||
wait_approval: "Thanks for signing up. We will notify you when your account has been approved."
|
||||
active: "Your account is activated and ready to use."
|
||||
activate_email: "<p>You're almost done! We sent an activation mail to <b>%{email}</b>. Please follow the instructions in the email to activate your account.</p><p>If it doesn't arrive, check your spam folder, or try to log in again to send another activation mail.</p>"
|
||||
activate_email: "<p>You're almost done! We sent an activation mail to <b>%{email}</b>. Please follow the instructions in the email to activate your account.</p><p>If it doesn't arrive, check your spam folder, or try to log in again to send another activation mail or to input a new email address.</p>"
|
||||
not_activated: "You can't log in yet. We sent an activation email to you. Please follow the instructions in the email to activate your account."
|
||||
not_allowed_from_ip_address: "You can't log in as %{username} from that IP address."
|
||||
admin_not_allowed_from_ip_address: "You can't log in as admin from that IP address."
|
||||
|
|
|
@ -308,6 +308,7 @@ Discourse::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
put "#{root_path}/update-activation-email" => "users#update_activation_email"
|
||||
get "#{root_path}/hp" => "users#get_honeypot_value"
|
||||
get "#{root_path}/admin-login" => "users#admin_login"
|
||||
put "#{root_path}/admin-login" => "users#admin_login"
|
||||
|
|
|
@ -1872,4 +1872,77 @@ describe UsersController do
|
|||
|
||||
end
|
||||
|
||||
|
||||
describe '.update_activation_email' do
|
||||
|
||||
it "raises an error with an invalid username" do
|
||||
xhr :put, :update_activation_email, {
|
||||
username: 'eviltrout',
|
||||
password: 'invalid-password',
|
||||
email: 'updatedemail@example.com'
|
||||
}
|
||||
expect(response).to_not be_success
|
||||
end
|
||||
|
||||
it "raises an error with an invalid password" do
|
||||
xhr :put, :update_activation_email, {
|
||||
username: Fabricate(:inactive_user).username,
|
||||
password: 'invalid-password',
|
||||
email: 'updatedemail@example.com'
|
||||
}
|
||||
expect(response).to_not be_success
|
||||
end
|
||||
|
||||
it "raises an error for an active user" do
|
||||
xhr :put, :update_activation_email, {
|
||||
username: Fabricate(:walter_white).username,
|
||||
password: 'letscook',
|
||||
email: 'updatedemail@example.com'
|
||||
}
|
||||
expect(response).to_not be_success
|
||||
end
|
||||
|
||||
it "raises an error when logged in" do
|
||||
log_in(:moderator)
|
||||
|
||||
xhr :put, :update_activation_email, {
|
||||
username: Fabricate(:inactive_user).username,
|
||||
password: 'qwerqwer123',
|
||||
email: 'updatedemail@example.com'
|
||||
}
|
||||
expect(response).to_not be_success
|
||||
end
|
||||
|
||||
it "raises an error when the new email is taken" do
|
||||
user = Fabricate(:user)
|
||||
|
||||
xhr :put, :update_activation_email, {
|
||||
username: Fabricate(:inactive_user).username,
|
||||
password: 'qwerqwer123',
|
||||
email: user.email
|
||||
}
|
||||
expect(response).to_not be_success
|
||||
end
|
||||
|
||||
it "can be updated" do
|
||||
user = Fabricate(:inactive_user)
|
||||
token = user.email_tokens.first
|
||||
|
||||
xhr :put, :update_activation_email, {
|
||||
username: user.username,
|
||||
password: 'qwerqwer123',
|
||||
email: 'updatedemail@example.com'
|
||||
}
|
||||
|
||||
expect(response).to be_success
|
||||
|
||||
user.reload
|
||||
expect(user.email).to eq('updatedemail@example.com')
|
||||
expect(user.email_tokens.where(email: 'updatedemail@example.com', expired: false)).to be_present
|
||||
|
||||
token.reload
|
||||
expect(token.expired?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -36,6 +36,7 @@ Fabricator(:inactive_user, from: :user) do
|
|||
name 'Inactive User'
|
||||
username 'inactive_user'
|
||||
email 'inactive@idontexist.com'
|
||||
password 'qwerqwer123'
|
||||
active false
|
||||
end
|
||||
|
||||
|
|
|
@ -41,16 +41,40 @@ test("sign in - not activated", () => {
|
|||
ok(!exists('.modal-body small'), 'it escapes the email address');
|
||||
});
|
||||
|
||||
click('.modal-body .resend-link');
|
||||
click('.modal-footer button.resend');
|
||||
andThen(() => {
|
||||
equal(find('.modal-body b').text(), '<small>current@example.com</small>');
|
||||
ok(!exists('.modal-body small'), 'it escapes the email address');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test("sign in - not activated - edit email", () => {
|
||||
visit("/");
|
||||
andThen(() => {
|
||||
click("header .login-button");
|
||||
andThen(() => {
|
||||
ok(exists('.login-modal'), "it shows the login modal");
|
||||
});
|
||||
|
||||
fillIn('#login-account-name', 'eviltrout');
|
||||
fillIn('#login-account-password', 'not-activated-edit');
|
||||
click('.modal-footer .btn-primary');
|
||||
click('.modal-footer button.edit-email');
|
||||
andThen(() => {
|
||||
equal(find('.activate-new-email').val(), 'current@example.com');
|
||||
equal(find('.modal-footer .btn-primary:disabled').length, 1, "must change email");
|
||||
});
|
||||
fillIn('.activate-new-email', 'different@example.com');
|
||||
andThen(() => {
|
||||
equal(find('.modal-footer .btn-primary:disabled').length, 0);
|
||||
});
|
||||
click(".modal-footer .btn-primary");
|
||||
andThen(() => {
|
||||
equal(find('.modal-body b').text(), 'different@example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("create account", () => {
|
||||
visit("/");
|
||||
|
|
|
@ -195,10 +195,18 @@ export default function() {
|
|||
current_email: '<small>current@example.com</small>' });
|
||||
}
|
||||
|
||||
if (data.password === 'not-activated-edit') {
|
||||
return response({ error: "not active",
|
||||
reason: "not_activated",
|
||||
sent_to_email: 'eviltrout@example.com',
|
||||
current_email: 'current@example.com' });
|
||||
}
|
||||
|
||||
return response(400, {error: 'invalid login'});
|
||||
});
|
||||
|
||||
this.post('/u/action/send_activation_email', success);
|
||||
this.put('/u/update-activation-email', success);
|
||||
|
||||
this.get('/u/hp.json', function() {
|
||||
return response({"value":"32faff1b1ef1ac3","challenge":"61a3de0ccf086fb9604b76e884d75801"});
|
||||
|
|
Loading…
Reference in New Issue
Block a user