diff --git a/app/assets/javascripts/confirm-new-email/bootstrap.js b/app/assets/javascripts/confirm-new-email/bootstrap.js
deleted file mode 100644
index db93c5ee55f..00000000000
--- a/app/assets/javascripts/confirm-new-email/bootstrap.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// discourse-skip-module
-(function () {
- require("confirm-new-email/confirm-new-email");
-})();
diff --git a/app/assets/javascripts/confirm-new-email/confirm-new-email.js b/app/assets/javascripts/confirm-new-email/confirm-new-email.js
deleted file mode 100644
index 053c9832767..00000000000
--- a/app/assets/javascripts/confirm-new-email/confirm-new-email.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { getWebauthnCredential } from "discourse/lib/webauthn";
-
-const security = document.getElementById("submit-security-key");
-if (security) {
- security.onclick = function (e) {
- e.preventDefault();
- getWebauthnCredential(
- document.getElementById("security-key-challenge").value,
- document
- .getElementById("security-key-allowed-credential-ids")
- .value.split(","),
- (credentialData) => {
- document.getElementById("security-key-credential").value =
- JSON.stringify(credentialData);
-
- e.target.closest("form").submit();
- },
- (errorMessage) => {
- document.getElementById(
- "security-key-error"
- ).innerHTML = `
- <% if @done %>
-
- <%= t 'change_email.confirmed' %>
-
-
- "><%= t('change_email.please_continue', site_name: SiteSetting.title) %>
-
- <% elsif @error %>
-
- <%= @error %>
-
- <% else %>
-
<%= t 'change_email.authorizing_new.title' %>
-
- <% if @change_request&.old_email %>
- <%= t 'change_email.authorizing_new.description' %>
- <% else %>
- <%= t 'change_email.authorizing_new.description_add' %>
- <% end %>
-
-
- <%= @to_email %>
-
-
- <%=form_tag(u_confirm_new_email_path, method: :put) do %>
- <%= hidden_field_tag 'token', params[:token] %>
- <%= hidden_field_tag 'second_factor_token', nil, id: 'security-key-credential' %>
-
-
- <% if @show_invalid_second_factor_error %>
-
<%= @invalid_second_factor_message %>
- <% end %>
-
- <% if @show_backup_codes %>
-
-
- <%= link_to t("login.second_factor_toggle.totp"), show_backup: "false" %>
-
- <% elsif @show_security_key %>
- <%= hidden_field_tag 'security_key_challenge', @security_key_challenge, id: 'security-key-challenge' %>
- <%= hidden_field_tag 'second_factor_method', UserSecondFactor.methods[:security_key] %>
- <%= hidden_field_tag 'security_key_allowed_credential_ids', @security_key_allowed_credential_ids.join(","), id: 'security-key-allowed-credential-ids' %>
-
-
- <% if @show_second_factor %>
- <%= link_to t("login.security_key_alternative"), show_totp: "true" %>
- <% end %>
- <% if @backup_codes_enabled %>
- <%= link_to t("login.second_factor_toggle.backup_code"), show_backup: "true" %>
- <% end %>
- <% elsif @show_second_factor %>
-
-
- <% if @backup_codes_enabled %>
- <%= link_to t("login.second_factor_toggle.backup_code"), show_backup: "true" %>
- <% end %>
- <% else %>
- <%= submit_tag t('change_email.confirm'), class: "btn btn-primary" %>
- <% end %>
- <%end%>
- <% end%>
-
- <%= preload_script "vendor" %>
- <%= preload_script "locales/#{I18n.locale}" %>
- <%- if ExtraLocalesController.client_overrides_exist? %>
- <%= preload_script_url ExtraLocalesController.url('overrides') %>
- <%- end %>
- <% # TODO: move all this logic into the ember app %>
- <%= preload_script "discourse/app/lib/webauthn" %>
- <%= preload_script "confirm-new-email/confirm-new-email" %>
- <%= preload_script "confirm-new-email/bootstrap" %>
-
diff --git a/app/views/users_email/show_confirm_old_email.html.erb b/app/views/users_email/show_confirm_old_email.html.erb
deleted file mode 100644
index 403103e8b26..00000000000
--- a/app/views/users_email/show_confirm_old_email.html.erb
+++ /dev/null
@@ -1,34 +0,0 @@
-
- <% if @almost_done %>
-
<%= t 'change_email.authorizing_old.almost_done_title' %>
-
- <%= t 'change_email.authorizing_old.almost_done_description' %>
-
- <% elsif @error %>
-
- <%= @error %>
-
- <% else %>
-
<%= t 'change_email.authorizing_old.title' %>
-
- <% if @change_request&.old_email %>
- <%= t 'change_email.authorizing_old.description' %>
-
-
- <%= t 'change_email.authorizing_old.old_email', email: @from_email %>
-
- <%= t 'change_email.authorizing_old.new_email', email: @to_email %>
- <% else %>
- <%= t 'change_email.authorizing_old.description_add' %>
-
-
- <%= @to_email %>
- <% end %>
-
-
- <%=form_tag(u_confirm_old_email_path, method: :put) do %>
- <%= hidden_field_tag 'token', params[:token] %>
- <%= submit_tag t('change_email.confirm'), class: "btn btn-primary" %>
- <% end %>
- <% end %>
-
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 02e001566c8..eef890db1c3 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -27,9 +27,6 @@ Rails.application.config.assets.precompile += %w[
break_string.js
service-worker.js
locales/i18n.js
- discourse/app/lib/webauthn.js
- confirm-new-email/confirm-new-email.js
- confirm-new-email/bootstrap.js
scripts/discourse-test-listen-boot
]
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 08e4edde2e4..8012d4f198d 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1559,6 +1559,18 @@ en:
success_via_admin: "We've sent an email to that address. The user will need to follow the confirmation instructions in the email."
success_staff: "We've sent an email to your current address. Please follow the confirmation instructions."
back_to_preferences: "Back to preferences"
+ confirm_success: "Your email has been updated."
+ confirm: "Confirm"
+ authorizing_new:
+ description: "Please confirm you would like your email address changed to:"
+ description_add: "Please confirm you would like to add an alternate email address:"
+ authorizing_old:
+ title: "Verify old email address"
+ description: "Please verify your old email address to continue changing your email:"
+ description_add: "Please verify your existing email address to continue adding an alternate address:"
+ old_email: "Old email: %{email}"
+ new_email: "New email: %{email}"
+ confirm_success: "We have sent an email to your new email address to confirm the change!"
change_avatar:
title: "Change your profile picture"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index e2a09c53cad..d8afaf477a2 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -919,9 +919,6 @@ en:
unknown: "unknown operating system"
change_email:
- wrong_account_error: "You are logged in the wrong account, please log out and try again."
- confirmed: "Your email has been updated."
- please_continue: "Continue to %{site_name}"
error: "There was an error changing your email address. Perhaps the address is already in use?"
doesnt_exist: "That email address is not associated with your account."
error_staged: "There was an error changing your email address. The address is already in use by a staged user."
@@ -929,20 +926,6 @@ en:
confirm: "Confirm"
max_secondary_emails_error: "You have reached the maximum allowed secondary emails limit."
- authorizing_new:
- title: "Confirm your new email"
- description: "Please confirm you would like your email address changed to:"
- description_add: "Please confirm you would like to add an alternate email address:"
-
- authorizing_old:
- title: "Change your email address"
- description: "Please confirm your email address change"
- description_add: "Please confirm you would like to add an alternate email address:"
- old_email: "Old email: %{email}"
- new_email: "New email: %{email}"
- almost_done_title: "Confirming new email address"
- almost_done_description: "We have sent an email to your new email address to confirm the change!"
-
associated_accounts:
revoke_failed: "Failed to revoke your account with %{provider_name}."
connected: "(connected)"
diff --git a/config/routes.rb b/config/routes.rb
index a9fa7597d8b..51ee33f4428 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -538,10 +538,10 @@ Discourse::Application.routes.draw do
)
get "#{root_path}/confirm-old-email/:token" => "users_email#show_confirm_old_email"
- put "#{root_path}/confirm-old-email" => "users_email#confirm_old_email"
+ put "#{root_path}/confirm-old-email/:token" => "users_email#confirm_old_email"
get "#{root_path}/confirm-new-email/:token" => "users_email#show_confirm_new_email"
- put "#{root_path}/confirm-new-email" => "users_email#confirm_new_email"
+ put "#{root_path}/confirm-new-email/:token" => "users_email#confirm_new_email"
get(
{
diff --git a/lib/second_factor/actions/confirm_email.rb b/lib/second_factor/actions/confirm_email.rb
new file mode 100644
index 00000000000..8e7cc9d841f
--- /dev/null
+++ b/lib/second_factor/actions/confirm_email.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module SecondFactor::Actions
+ class ConfirmEmail < Base
+ def no_second_factors_enabled!(params)
+ # handled in controller
+ end
+
+ def second_factor_auth_required!(params)
+ {
+ callback_params: {
+ token: params[:token],
+ },
+ redirect_url:
+ (
+ if @current_user
+ "#{Discourse.base_path}/my/preferences/account"
+ else
+ "#{Discourse.base_path}/login"
+ end
+ ),
+ }
+ end
+
+ def second_factor_auth_completed!(callback_params)
+ # handled in controller
+ end
+ end
+end
diff --git a/spec/requests/users_email_controller_spec.rb b/spec/requests/users_email_controller_spec.rb
index c90dc6904ca..076d23f5343 100644
--- a/spec/requests/users_email_controller_spec.rb
+++ b/spec/requests/users_email_controller_spec.rb
@@ -24,10 +24,9 @@ RSpec.describe UsersEmailController do
it "errors out for invalid tokens" do
sign_in(user)
- get "/u/confirm-new-email/invalidtoken"
+ get "/u/confirm-new-email/invalidtoken.json"
- expect(response.status).to eq(200)
- expect(response.body).to include(I18n.t("change_email.already_done"))
+ expect(response.status).to eq(404)
end
it "does not change email if accounts mismatch for a signed in user" do
@@ -38,7 +37,8 @@ RSpec.describe UsersEmailController do
sign_in(moderator)
- put "/u/confirm-new-email", params: { token: "#{email_token.token}" }
+ put "/u/confirm-new-email/#{email_token.token}.json"
+ expect(response.status).to eq(404)
expect(user.reload.email).to eq(old_email)
end
@@ -50,212 +50,17 @@ RSpec.describe UsersEmailController do
updater.change_to("bubblegum@adventuretime.ooo")
end
- it "includes security_key_allowed_credential_ids in a hidden field" do
- key1 = Fabricate(:user_security_key_with_random_credential, user: user)
- key2 = Fabricate(:user_security_key_with_random_credential, user: user)
-
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
-
- doc = Nokogiri.HTML5(response.body)
- credential_ids = doc.css("#security-key-allowed-credential-ids").first["value"].split(",")
- expect(credential_ids).to contain_exactly(key1.credential_id, key2.credential_id)
- end
-
it "confirms with a correct token" do
user.user_stat.update_columns(bounce_score: 42, reset_bounce_score_after: 1.week.from_now)
- put "/u/confirm-new-email", params: { token: "#{updater.change_req.new_email_token.token}" }
+ put "/u/confirm-new-email/#{updater.change_req.new_email_token.token}.json"
- expect(response.status).to eq(302)
- expect(response.redirect_url).to include("done")
+ expect(response.status).to eq(200)
user.reload
expect(user.user_stat.bounce_score).to eq(0)
expect(user.user_stat.reset_bounce_score_after).to eq(nil)
expect(user.email).to eq("bubblegum@adventuretime.ooo")
end
-
- context "when second factor is required" do
- fab!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) }
- fab!(:backup_code) { Fabricate(:user_second_factor_backup, user: user) }
-
- it "requires a second factor token" do
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
-
- expect(response.status).to eq(200)
- expect(response.body).to include(I18n.t("login.second_factor_title"))
- expect(response.body).not_to include(I18n.t("login.invalid_second_factor_code"))
- end
-
- it "requires a backup token" do
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_backup=true"
-
- expect(response.status).to eq(200)
- expect(response.body).to include(I18n.t("login.second_factor_backup_title"))
- end
-
- it "adds an error on a second factor attempt" do
- put "/u/confirm-new-email",
- params: {
- token: updater.change_req.new_email_token.token,
- second_factor_token: "000000",
- second_factor_method: UserSecondFactor.methods[:totp],
- }
-
- expect(response.status).to eq(302)
- expect(flash[:invalid_second_factor]).to eq(true)
- end
-
- it "confirms with a correct second token" do
- put "/u/confirm-new-email",
- params: {
- second_factor_token: ROTP::TOTP.new(second_factor.data).now,
- second_factor_method: UserSecondFactor.methods[:totp],
- token: updater.change_req.new_email_token.token,
- }
-
- expect(response.status).to eq(302)
- expect(user.reload.email).to eq("bubblegum@adventuretime.ooo")
- end
-
- context "with rate limiting" do
- before { RateLimiter.enable }
-
- use_redis_snapshotting
-
- it "rate limits by IP" do
- freeze_time
-
- 6.times do
- put "/u/confirm-new-email",
- params: {
- token: "blah",
- second_factor_token: "000000",
- second_factor_method: UserSecondFactor.methods[:totp],
- }
-
- expect(response.status).to eq(302)
- end
-
- put "/u/confirm-new-email",
- params: {
- token: "blah",
- second_factor_token: "000000",
- second_factor_method: UserSecondFactor.methods[:totp],
- }
-
- expect(response.status).to eq(429)
- end
-
- it "rate limits by username" do
- freeze_time
-
- 6.times do |x|
- user.email_change_requests.last.update(
- change_state: EmailChangeRequest.states[:complete],
- )
- put "/u/confirm-new-email",
- params: {
- token: updater.change_req.new_email_token.token,
- second_factor_token: "000000",
- second_factor_method: UserSecondFactor.methods[:totp],
- },
- env: {
- REMOTE_ADDR: "1.2.3.#{x}",
- }
-
- expect(response.status).to eq(302)
- end
-
- user.email_change_requests.last.update(
- change_state: EmailChangeRequest.states[:authorizing_new],
- )
- put "/u/confirm-new-email",
- params: {
- token: updater.change_req.new_email_token.token,
- second_factor_token: "000000",
- second_factor_method: UserSecondFactor.methods[:totp],
- },
- env: {
- REMOTE_ADDR: "1.2.3.4",
- }
-
- expect(response.status).to eq(429)
- end
- end
- end
-
- context "when security key is required" do
- fab!(:user_security_key) do
- Fabricate(
- :user_security_key,
- user: user,
- credential_id: valid_security_key_data[:credential_id],
- public_key: valid_security_key_data[:public_key],
- )
- end
-
- before { simulate_localhost_webauthn_challenge }
-
- it "requires a security key" do
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
-
- expect(response.status).to eq(200)
- expect(response.body).to include(I18n.t("login.security_key_authenticate"))
- expect(response.body).to include(I18n.t("login.security_key_description"))
- end
-
- context "if the user has a TOTP enabled and wants to use that instead" do
- before { Fabricate(:user_second_factor_totp, user: user) }
-
- it "allows entering the totp code instead" do
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_totp=true"
-
- expect(response.status).to eq(200)
- expect(response.body).to include(I18n.t("login.second_factor_title"))
- expect(response.body).not_to include(I18n.t("login.security_key_authenticate"))
- end
- end
-
- it "adds an error on a security key attempt" do
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
- put "/u/confirm-new-email",
- params: {
- token: updater.change_req.new_email_token.token,
- second_factor_token: "{}",
- second_factor_method: UserSecondFactor.methods[:security_key],
- }
-
- expect(response.status).to eq(302)
- expect(flash[:invalid_second_factor]).to eq(true)
- end
-
- it "confirms with a correct security key token" do
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
- put "/u/confirm-new-email",
- params: {
- token: updater.change_req.new_email_token.token,
- second_factor_token: valid_security_key_auth_post_data.to_json,
- second_factor_method: UserSecondFactor.methods[:security_key],
- }
-
- expect(response.status).to eq(302)
- expect(user.reload.email).to eq("bubblegum@adventuretime.ooo")
- end
-
- context "if the security key data JSON is garbled" do
- it "raises an invalid parameters error" do
- get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
- put "/u/confirm-new-email",
- params: {
- token: updater.change_req.new_email_token.token,
- second_factor_token: "{someweird: 8notjson}",
- second_factor_method: UserSecondFactor.methods[:security_key],
- }
-
- expect(response.status).to eq(400)
- end
- end
- end
end
it "destroys email tokens associated with the old email after the new email is confirmed" do
@@ -268,7 +73,8 @@ RSpec.describe UsersEmailController do
updater.change_to("bubblegum@adventuretime.ooo")
sign_in(user)
- put "/u/confirm-new-email", params: { token: "#{updater.change_req.new_email_token.token}" }
+ put "/u/confirm-new-email/#{updater.change_req.new_email_token.token}.json"
+ expect(response.status).to eq(200)
new_password = SecureRandom.hex
put "/u/password-reset/#{email_token.token}.json", params: { password: new_password }
@@ -281,20 +87,12 @@ RSpec.describe UsersEmailController do
end
describe "#confirm-old-email" do
- it "redirects to login for signed out accounts" do
- get "/u/confirm-old-email/invalidtoken"
-
- expect(response.status).to eq(302)
- expect(response.redirect_url).to eq("http://test.localhost/login")
- end
-
it "errors out for invalid tokens" do
sign_in(user)
- get "/u/confirm-old-email/invalidtoken"
+ get "/u/confirm-old-email/invalidtoken.json"
- expect(response.status).to eq(200)
- expect(response.body).to include(I18n.t("change_email.already_done"))
+ expect(response.status).to eq(404)
end
it "bans change when accounts do not match" do
@@ -302,10 +100,9 @@ RSpec.describe UsersEmailController do
updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator)
email_change_request = updater.change_to("bubblegum@adventuretime.ooo")
- get "/u/confirm-old-email/#{email_change_request.old_email_token.token}"
+ get "/u/confirm-old-email/#{email_change_request.old_email_token.token}.json"
- expect(response.status).to eq(200)
- expect(body).to include("alert-error")
+ expect(response.status).to eq(403)
end
context "with valid old token" do
@@ -314,17 +111,15 @@ RSpec.describe UsersEmailController do
updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator)
email_change_request = updater.change_to("bubblegum@adventuretime.ooo")
- get "/u/confirm-old-email/#{email_change_request.old_email_token.token}"
+ get "/u/confirm-old-email/#{email_change_request.old_email_token.token}.json"
expect(response.status).to eq(200)
- body = CGI.unescapeHTML(response.body)
- expect(body).to include(I18n.t("change_email.authorizing_old.title"))
- expect(body).to include(I18n.t("change_email.authorizing_old.description"))
+ expect(response.parsed_body["old_email"]).to eq(moderator.email)
+ expect(response.parsed_body["new_email"]).to eq("bubblegum@adventuretime.ooo")
- put "/u/confirm-old-email", params: { token: email_change_request.old_email_token.token }
+ put "/u/confirm-old-email/#{email_change_request.old_email_token.token}.json"
- expect(response.status).to eq(302)
- expect(response.redirect_url).to include("done=true")
+ expect(response.status).to eq(200)
end
end
end
diff --git a/spec/system/email_change_spec.rb b/spec/system/email_change_spec.rb
new file mode 100644
index 00000000000..a2683fc5b3d
--- /dev/null
+++ b/spec/system/email_change_spec.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+describe "Changing email", type: :system do
+ fab!(:password) { "mysupersecurepassword" }
+ fab!(:user) { Fabricate(:user, active: true, password: password) }
+ let(:new_email) { "newemail@example.com" }
+ let(:user_preferences_security_page) { PageObjects::Pages::UserPreferencesSecurity.new }
+
+ before { Jobs.run_immediately! }
+
+ def generate_confirm_link
+ visit "/my/preferences/account"
+
+ email_dropdown = PageObjects::Components::SelectKit.new(".email-dropdown")
+ expect(email_dropdown.visible?).to eq(true)
+ email_dropdown.select_row_by_value("updateEmail")
+
+ find("#change-email").fill_in with: "newemail@example.com"
+
+ find(".save-button button").click
+
+ wait_for(timeout: 5) { ActionMailer::Base.deliveries.count === 1 }
+
+ if user.admin?
+ get_link_from_email(:old)
+ else
+ get_link_from_email(:new)
+ end
+ end
+
+ def get_link_from_email(type)
+ mail = ActionMailer::Base.deliveries.last
+ expect(mail.to).to contain_exactly(type == :new ? new_email : user.email)
+
+ mail.body.to_s[%r{/u/confirm-#{type}-email/\S+}, 0]
+ end
+
+ it "allows regular user to change their email" do
+ sign_in user
+
+ visit generate_confirm_link
+
+ find(".confirm-new-email .btn-primary").click
+
+ expect(page).to have_css(".dialog-body", text: I18n.t("js.user.change_email.confirm_success"))
+ find(".dialog-footer .btn-primary").click
+
+ expect(user.reload.email).to eq(new_email)
+ end
+
+ it "works when user has totp 2fa" do
+ second_factor = Fabricate(:user_second_factor_totp, user: user)
+ sign_in user
+
+ visit generate_confirm_link
+
+ find(".confirm-new-email .btn-primary").click
+
+ find(".second-factor-token-input").fill_in with: second_factor.totp_object.now
+
+ find("button[type=submit]").click
+
+ try_until_success { expect(current_url).to match("/u/#{user.username}/preferences/account") }
+
+ expect(user.reload.email).to eq(new_email)
+ end
+
+ it "works when user has webauthn 2fa" do
+ sign_in user
+
+ DiscourseWebauthn.stubs(:origin).returns(current_host + ":" + Capybara.server_port.to_s)
+ options =
+ ::Selenium::WebDriver::VirtualAuthenticatorOptions.new(
+ user_verification: true,
+ user_verified: true,
+ resident_key: true,
+ )
+ authenticator = page.driver.browser.add_virtual_authenticator(options)
+
+ user_preferences_security_page.visit(user)
+ user_preferences_security_page.visit_second_factor(password)
+
+ find(".security-key .new-security-key").click
+ expect(user_preferences_security_page).to have_css("input#security-key-name")
+
+ find(".d-modal__body input#security-key-name").fill_in(with: "First Key")
+ find(".add-security-key").click
+
+ expect(user_preferences_security_page).to have_css(".security-key .second-factor-item")
+
+ visit generate_confirm_link
+
+ find(".confirm-new-email .btn-primary").click
+
+ find("#security-key-authenticate-button").click
+
+ try_until_success { expect(current_url).to match("/u/#{user.username}/preferences/account") }
+
+ expect(user.reload.email).to eq(new_email)
+ end
+
+ it "does not require login to verify" do
+ second_factor = Fabricate(:user_second_factor_totp, user: user)
+ sign_in user
+
+ confirm_link = generate_confirm_link
+
+ Capybara.reset_sessions! # log out
+
+ visit confirm_link
+
+ find(".confirm-new-email .btn-primary").click
+
+ find(".second-factor-token-input").fill_in with: second_factor.totp_object.now
+
+ find("button[type=submit]").click
+
+ try_until_success { expect(current_url).to match("/latest") }
+
+ expect(user.reload.email).to eq(new_email)
+ end
+
+ it "makes admins verify old email" do
+ user.update!(admin: true)
+ sign_in user
+
+ confirm_old_link = generate_confirm_link
+
+ # Confirm old email
+ visit confirm_old_link
+ find(".confirm-old-email .btn-primary").click
+ expect(page).to have_css(
+ ".dialog-body",
+ text: I18n.t("js.user.change_email.authorizing_old.confirm_success"),
+ )
+ find(".dialog-footer .btn-primary").click
+
+ # Confirm new email
+ wait_for(timeout: 5) { ActionMailer::Base.deliveries.count === 2 }
+ confirm_new_link = get_link_from_email(:new)
+
+ visit confirm_new_link
+
+ find(".confirm-new-email .btn-primary").click
+
+ expect(page).to have_css(".dialog-body", text: I18n.t("js.user.change_email.confirm_success"))
+ find(".dialog-footer .btn-primary").click
+
+ expect(user.reload.email).to eq(new_email)
+ end
+
+ it "allows admin to verify old email while logged out" do
+ user.update!(admin: true)
+ sign_in user
+
+ confirm_old_link = generate_confirm_link
+
+ Capybara.reset_sessions! # log out
+
+ # Confirm old email
+ visit confirm_old_link
+ find(".confirm-old-email .btn-primary").click
+ expect(page).to have_css(
+ ".dialog-body",
+ text: I18n.t("js.user.change_email.authorizing_old.confirm_success"),
+ )
+ find(".dialog-footer .btn-primary").click
+
+ # Confirm new email
+ wait_for(timeout: 5) { ActionMailer::Base.deliveries.count === 2 }
+ confirm_new_link = get_link_from_email(:new)
+
+ visit confirm_new_link
+
+ find(".confirm-new-email .btn-primary").click
+
+ expect(page).to have_css(".dialog-body", text: I18n.t("js.user.change_email.confirm_success"))
+ find(".dialog-footer .btn-primary").click
+
+ expect(user.reload.email).to eq(new_email)
+ end
+end