diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index ccbb3eaf692..b32f0b91940 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -20,7 +20,8 @@ class Admin::EmailTemplatesController < Admin::AdminController "system_messages.unblocked", "system_messages.user_automatically_blocked", "system_messages.welcome_invite", "system_messages.welcome_user", "test_mailer", "user_notifications.account_created", "user_notifications.admin_login", - "user_notifications.authorize_email", "user_notifications.forgot_password", + "user_notifications.confirm_new_email", "user_notifications.confirm_old_email", + "user_notifications.notify_old_email", "user_notifications.forgot_password", "user_notifications.set_password", "user_notifications.signup", "user_notifications.signup_after_approval", "user_notifications.user_invited_to_private_message_pm", diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 08564f52fb6..586e7fded0b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -5,7 +5,7 @@ require_dependency 'rate_limiter' class UsersController < ApplicationController skip_before_filter :authorize_mini_profiler, only: [:avatar] - skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :account_created, :activate_account, :perform_account_activation, :authorize_email, :user_preferences_redirect, :avatar, :my_redirect, :toggle_anon, :admin_login] + skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :account_created, :activate_account, :perform_account_activation, :user_preferences_redirect, :avatar, :my_redirect, :toggle_anon, :admin_login] before_filter :ensure_logged_in, only: [:username, :update, :user_preferences_redirect, :upload_user_image, :pick_avatar, :destroy_user_image, :destroy, :check_emails] before_filter :respond_to_suspicious_request, only: [:create] @@ -21,7 +21,6 @@ class UsersController < ApplicationController :activate_account, :perform_account_activation, :send_activation_email, - :authorize_email, :password_reset, :confirm_email_token, :admin_login] @@ -471,16 +470,6 @@ class UsersController < ApplicationController end end - def authorize_email - expires_now() - if @user = EmailToken.confirm(params[:token]) - log_on_user(@user) - else - flash[:error] = I18n.t('change_email.error') - end - render layout: 'no_ember' - end - def account_created @message = session['user_created_message'] || I18n.t('activation.missing_session') expires_now diff --git a/app/controllers/users_email_controller.rb b/app/controllers/users_email_controller.rb index c7e501f52a7..da4ce692340 100644 --- a/app/controllers/users_email_controller.rb +++ b/app/controllers/users_email_controller.rb @@ -1,9 +1,13 @@ require_dependency 'rate_limiter' require_dependency 'email_validator' +require_dependency 'email_updater' class UsersEmailController < ApplicationController - before_filter :ensure_logged_in + before_filter :ensure_logged_in, only: [:index, :update] + + skip_before_filter :check_xhr, only: [:confirm] + skip_before_filter :redirect_to_login_if_required, only: [:confirm] def index end @@ -11,31 +15,32 @@ class UsersEmailController < ApplicationController def update params.require(:email) user = fetch_user_from_params - guardian.ensure_can_edit_email!(user) - lower_email = Email.downcase(params[:email]).strip RateLimiter.new(user, "change-email-hr-#{request.remote_ip}", 6, 1.hour).performed! RateLimiter.new(user, "change-email-min-#{request.remote_ip}", 3, 1.minute).performed! - EmailValidator.new(attributes: :email).validate_each(user, :email, lower_email) - return render_json_error(user.errors.full_messages) if user.errors[:email].present? + updater = EmailUpdater.new(guardian, user) + updater.change_to(params[:email]) - # Raise an error if the email is already in use - return render_json_error(I18n.t('change_email.error')) if User.find_by_email(lower_email) - - email_token = user.email_tokens.create(email: lower_email) - Jobs.enqueue( - :user_email, - to_address: lower_email, - type: :authorize_email, - user_id: user.id, - email_token: email_token.token - ) + if updater.errors.present? + return render_json_error(updater.errors.full_messages) + end render nothing: true rescue RateLimiter::LimitExceeded render_json_error(I18n.t("rate_limiter.slow_down")) end + def confirm + expires_now + updater = EmailUpdater.new + @update_result = updater.confirm(params[:token]) + + # Log in the user if the process is complete (and they're not logged in) + log_on_user(updater.user) if @update_result == :complete + + render layout: 'no_ember' + end + end diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index 9ef71e08670..ca7b73dc710 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -109,6 +109,10 @@ module Jobs email_args[:email_token] = email_token end + if type == 'notify_old_email' + email_args[:new_email] = user.email + end + message = UserNotifications.send(type, user, email_args) # Update the to address if we have a custom one diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index d63dd23b92b..943b2a127a5 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -23,9 +23,23 @@ class UserNotifications < ActionMailer::Base new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url, locale: locale)) end - def authorize_email(user, opts={}) + def notify_old_email(user, opts={}) build_email(user.email, - template: "user_notifications.authorize_email", + template: "user_notifications.notify_old_email", + locale: user_locale(user), + new_email: opts[:new_email]) + end + + def confirm_old_email(user, opts={}) + build_email(user.email, + template: "user_notifications.confirm_old_email", + locale: user_locale(user), + email_token: opts[:email_token]) + end + + def confirm_new_email(user, opts={}) + build_email(user.email, + template: "user_notifications.confirm_new_email", locale: user_locale(user), email_token: opts[:email_token]) end diff --git a/app/models/email_change_request.rb b/app/models/email_change_request.rb new file mode 100644 index 00000000000..64d1fa104f8 --- /dev/null +++ b/app/models/email_change_request.rb @@ -0,0 +1,9 @@ +class EmailChangeRequest < ActiveRecord::Base + belongs_to :old_email_token, class_name: 'EmailToken' + belongs_to :new_email_token, class_name: 'EmailToken' + + def self.states + @states ||= Enum.new(authorizing_old: 1, authorizing_new: 2, complete: 3) + end + +end diff --git a/app/models/email_token.rb b/app/models/email_token.rb index 146c3d7c5e3..c919b619033 100644 --- a/app/models/email_token.rb +++ b/app/models/email_token.rb @@ -41,28 +41,41 @@ class EmailToken < ActiveRecord::Base return token.present? && token =~ /[a-f0-9]{#{token.length/2}}/i end - def self.confirm(token) - return unless valid_token_format?(token) + def self.atomic_confirm(token) + failure = { success: false } + return failure unless valid_token_format?(token) email_token = confirmable(token) - return if email_token.blank? + return failure if email_token.blank? user = email_token.user + failure[:user] = user + row_count = EmailToken.where(id: email_token.id, expired: false).update_all 'confirmed = true' + if row_count == 1 + return { success: true, user: user, email_token: email_token } + end + + return failure + end + + def self.confirm(token) User.transaction do - row_count = EmailToken.where(id: email_token.id, expired: false).update_all 'confirmed = true' - if row_count == 1 + result = atomic_confirm(token) + user = result[:user] + if result[:success] # If we are activating the user, send the welcome message user.send_welcome_message = !user.active? user.active = true - user.email = email_token.email + user.email = result[:email_token].email user.save! end - end - # redeem invite, if available - return User.find_by(email: Email.downcase(user.email)) if Invite.redeem_from_email(user.email).present? - user + if user + return User.find_by(email: Email.downcase(user.email)) if Invite.redeem_from_email(user.email).present? + user + end + end rescue ActiveRecord::RecordInvalid # If the user's email is already taken, just return nil (failure) end diff --git a/app/models/user.rb b/app/models/user.rb index 667cfb6b55f..fb865b213c4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,6 +36,7 @@ class User < ActiveRecord::Base has_many :uploads has_many :warnings has_many :user_archived_messages, dependent: :destroy + has_many :email_change_requests, dependent: :destroy has_one :user_option, dependent: :destroy diff --git a/app/views/users/authorize_email.html.erb b/app/views/users/authorize_email.html.erb deleted file mode 100644 index 8eef69bc663..00000000000 --- a/app/views/users/authorize_email.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -
- <%if flash[:error]%> -
- <%=flash[:error]%> -
- <%else%> -

<%= t 'change_email.confirmed' %>

-
- <%= t('change_email.please_continue', site_name: SiteSetting.title) %> - <%= render partial: 'auto_redirect_home' %> - <%end%> -
\ No newline at end of file diff --git a/app/views/users_email/confirm.html.erb b/app/views/users_email/confirm.html.erb new file mode 100644 index 00000000000..f02bb9fbe9d --- /dev/null +++ b/app/views/users_email/confirm.html.erb @@ -0,0 +1,15 @@ +
+ <% if @update_result == :authorizing_new %> +

<%= t 'change_email.authorizing_old.title' %>

+
+

<%= t 'change_email.authorizing_old.description' %>

+ <% elsif @update_result == :complete %> +

<%= t 'change_email.confirmed' %>

+
+ <%= t('change_email.please_continue', site_name: SiteSetting.title) %> + <% else %> +
+ <%=t 'change_email.error' %> +
+ <% end %> +
diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 8a9a652988b..fb8cdfee185 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -1874,7 +1874,7 @@ ar: انقر على الرابط التالي لاختيار كلمة مرور لحسابك الجديد: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] تأكيد البريد الإلكتروني الجديد " text_body_template: | قم بتاكيد عنوان بريدك الالكتروني لـ %{site_name} عن طريق الضغط علي الرابط التالي: diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index bcce781f8c6..4f4d692b35d 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -1029,7 +1029,7 @@ bs_BA: Kliknite na link ispod da kreirate šifru za vaš nalog: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Potvrdite vaš novi email" text_body_template: | Confirm your new email address for %{site_name} by clicking on the following link: diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index 2269a88b9ae..44cacdfe134 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -805,7 +805,7 @@ cs: %{base_url}/users/password-reset/%{email_token} admin_login: subject_template: "[%{site_name}] Přihlášení" - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Potvrďte vaši novou emailovou adresu" text_body_template: | Potvrďte vaši novou emailovou adresu pro %{site_name} kliknutím na následující odkaz: diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index dd05eed3cb4..ff84e1fa6c7 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -780,7 +780,7 @@ da: %{base_url}/users/password-reset/%{email_token} account_created: subject_template: "[%{site_name}] Din nye konto" - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Bekræft din nye e-mail-adresse" text_body_template: | Bekræft din nye e-mail-adresse på %{site_name} ved at klikke på følgende link: diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index a65ccd10976..100250b1f35 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -1573,7 +1573,7 @@ de: Klicke auf den folgenden Link, um ein Passwort für dein neues Konto festzulegen: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Bestätige deine neue Mailadresse" text_body_template: | Um deine Mailadresse auf %{site_name} zu bestätigen, klicke auf den folgenden Link: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index efbacec9ab0..e76ba73b4a8 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -517,6 +517,10 @@ en: 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?" + already_done: "Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?" + authorizing_old: + title: "Thanks for confirming your current email address" + description: "We're now emailing your new address for confirmation." activation: action: "Click here to activate your account" @@ -2245,13 +2249,35 @@ en: Click the following link to choose a password for your new account: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirm your new email address" text_body_template: | Confirm your new email address for %{site_name} by clicking on the following link: %{base_url}/users/authorize-email/%{email_token} + confirm_old_email: + subject_template: "[%{site_name}] Confirm your current email address" + text_body_template: | + Before we can change your email address, we need you to confirm that you control + the current email account. After you complete this step, we will have you confirm + the new email address. + + Confirm your current email address for %{site_name} by clicking on the following link: + + %{base_url}/users/authorize-email/%{email_token} + + notify_old_email: + subject_template: "[%{site_name}] Your email address has been changed" + text_body_template: | + This is an automated message to let you know that your email address for + %{site_name} has been changed. If this was done in error, please contact + a site administrator. + + Your email address has been changed to: + + %{new_email} + signup_after_approval: subject_template: "You've been approved on %{site_name}!" text_body_template: | diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 4a666bfea9e..83e4627ac03 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -1702,7 +1702,7 @@ es: Pulsa en el siguiente enlace para escoger una contraseña para tu nueva cuenta: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirma tu nueva dirección de email" text_body_template: | Confirma tu nueva dirección de email para %{site_name} haciendo clic en el siguiente enlace: diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index d96cbb7f1ef..9de4a7cc6b3 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -1297,7 +1297,7 @@ fa_IR: account_created: subject_template: "[%{site_name}] حساب کاربری جدید شما" text_body_template: "حساب کاربری جدید برای شما ساخته شد در %{site_name}\n\nبر روری پیوند پیش رو کلیک کنید برای انتخاب رمز برای حساب کاربری جدیدتان: \n\n%{base_url}/users/password-reset/%{email_token}\n" - authorize_email: + confirm_new_email: subject_template: "آدرس ایمیل جدید را تایید کنید برای [%{site_name}]" text_body_template: "آدرس ایمیل جدید را تایید کنید برای %{site_name} با کلیک کردن بر پیوند پیش رو : \n\n\n%{base_url}/users/authorize-email/%{email_token}\n" signup_after_approval: diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index efcf5c357da..ae2c4febebe 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -1829,7 +1829,7 @@ fi: Klikkaa seuraavaa linkkiä asettaaksesi salasanan uudelle tunnuksellesi: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Vahvista uusi sähköpostiosoite" text_body_template: | Vahvista uusi sähköpostiosoite sivustolle %{site_name} klikkaamalla alla olevaa linkkiä: diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 8adea1b9834..b6d224561f8 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -1793,7 +1793,7 @@ fr: Cliquez sur le lien ci-dessous pour choisir un mot de passe pour votre nouveau compte : %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirmation de votre nouvelle adresse de courriel" text_body_template: | Confirmez votre nouvelle adresse de courriel pour %{site_name} en cliquant sur le lien suivant : diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index e79e6d50eb2..6b645d3f54f 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -1472,7 +1472,7 @@ he: הקישו על הקישור המצורף כדי להגדיר סיסמא לחשבונך החדש: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirm your new email address" text_body_template: | Confirm your new email address for %{site_name} by clicking on the following link: diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 969ba7c39d3..e564607c9ba 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -1207,7 +1207,7 @@ it: Fai clic sul seguente collegamento per scegliere una password per il tuo nuovo account: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Conferma il tuo nuovo indirizzo email" text_body_template: | Conferma il tuo nuovo indirizzo email per %{site_name} cliccando sul seguente collegamento: diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 98e5f8cdd5e..c9fe9b9d3ce 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -1319,7 +1319,7 @@ ja: 以下のリンクをクリックしてパスワードを設定してください: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] メールアドレスの確認" text_body_template: | 次のリンクをクリックして %{site_name} 用のメールアドレスを確認してください: diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index dc8cfc2971b..055850ef95f 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -1424,7 +1424,7 @@ ko: 비밀번호 설정 페이지: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] 이메일 확인" text_body_template: | %{site_name} 사이트에서 사용할 새로운 이메일을 아래 링크를 클릭하여 확인하세요: diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 99daac07329..513fa6abf40 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -1381,7 +1381,7 @@ nl: Klik op deze link om een wachtwoord in te stellen voor je nieuwe account: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Bevestig je nieuwe e-mailadres" text_body_template: | Bevestig je nieuwe e-mailadres voor %{site_name} door op de volgende link te klikken: diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index fe7f69a1817..e837906227e 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -1143,7 +1143,7 @@ pl_PL: Kliknij na linku poniżej, aby ustawić swoje hasło: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Potwierdź nowy adres email" text_body_template: | Potwierdź Twój nowy adres email na forum %{site_name} przez kliknięcie na poniższy link: diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index bcd2b1a06d5..9df7a566e92 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -1812,7 +1812,7 @@ pt: Clique na hiperligação seguinte para escolher uma palavra-passe para a sua nova conta. %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirme o seu novo endereço de email" text_body_template: | Confirme o seu novo endereço de email para %{site_name} ao clicar na seguinte hiperligação: diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index e335373c0c6..9693df9c36c 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -1417,7 +1417,7 @@ pt_BR: Clique no link a seguir para escolher uma senha para a sua nova conta: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirma o seu novo endereço de email" text_body_template: | Confirma o seu endereço de email novo para %{site_name} clicando no seguinte link: diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 8904158bbdd..1ccbc6ec441 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -950,7 +950,7 @@ ro: %{base_url}/users/password-reset/%{email_token} admin_login: subject_template: "[%{site_name}] Autentificare" - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirmă noua adresă de email" text_body_template: | Confirmă noua adresă de email pentru %{site_name} făcând click pe următoarea adresă: diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 3e7c8949165..10b35f2b2a2 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -1522,7 +1522,7 @@ ru: Чтобы установить пароль, пройдите по следующей ссылке: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Подтвердите новый адрес электронной почты" text_body_template: | Подтвердите ваш новый адрес электронной почты для сайта %{site_name}, перейдя по следующей ссылке: diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 6cc8e3a3b42..11ba25a367c 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -1790,7 +1790,7 @@ sk: Kliknite na nasledujúci odkaz pre nastavenie hesla k Vášmu novému účtu: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Potvrďte Vašu novú email adresu" text_body_template: | Potvrďte Vašu novú emailovú adresu pre %{site_name} kliknutím na nasledujúci odkaz: diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index a2c0e784690..9c06a0b8bf8 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -1493,7 +1493,7 @@ sq: Click the following link to choose a password for your new account: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirm your new email address" text_body_template: | Confirm your new email address for %{site_name} by clicking on the following link: diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index b5eb9d97c8a..ab2f2cd8c44 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -983,7 +983,7 @@ sv: subject_template: "[%{site_name}] Logga in" account_created: subject_template: "[%{site_name}] Ditt nya konto" - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Confirm your new email address" text_body_template: | Confirm your new email address for %{site_name} by clicking on the following link: diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 0f044e9a0ca..ea2e66abdc7 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -1421,7 +1421,7 @@ tr_TR: Yeni hesabınıza ait bir parola oluşturmak için aşağıdaki bağlantıya tıklayın: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Yeni e-posta adresinizi onaylayın" text_body_template: | Aşağıdaki bağlantıya tıklayarak %{site_name} sitesindeki yeni e-posta adresinizi onaylayın: diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index c8a3312658c..a4b9b794082 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -457,7 +457,7 @@ uk: Перейдіть за посиланням, щоб обрати пароль: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Підтвердіть свою електронну скриньку" text_body_template: | Підтвердіть свою нову електронну скриньку для сайта %{site_name}, перейшовши за посиланням: diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index 15dc847ba85..ab2a5822eb4 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -1111,7 +1111,7 @@ vi: subject_template: "[%{site_name}] Đăng nhập" account_created: subject_template: "[%{site_name}] Tài khoản mới" - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] Xác nhận địa chỉ email mới của bạn" signup_after_approval: subject_template: "Bạn đã được kiểm duyệt ở %{site_name}!" diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 1a9b11270fe..1e86066dae9 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -1850,7 +1850,7 @@ zh_CN: 点击下面的链接来为新账户设置密码: %{base_url}/users/password-reset/%{email_token} - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] 确认你的新电子邮箱地址" text_body_template: | 点击下面的链接来确认你在 %{site_name} 上的新电子邮箱地址: diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 2a93ce0121e..f04a2abed44 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -943,7 +943,7 @@ zh_TW: subject_template: "[%{site_name}] 設定密碼" account_created: subject_template: "[%{site_name}] 你的新帳號" - authorize_email: + confirm_new_email: subject_template: "[%{site_name}] 確認你的新電子郵箱位址" text_body_template: | 點擊下面的連結來確認你在 %{site_name} 上的新電子郵箱位址: diff --git a/config/routes.rb b/config/routes.rb index a33b0b80e82..d28299a7bd4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -274,7 +274,7 @@ Discourse::Application.routes.draw do put "users/password-reset/:token" => "users#password_reset" get "users/activate-account/:token" => "users#activate_account" put "users/activate-account/:token" => "users#perform_account_activation", as: 'perform_activate_account' - get "users/authorize-email/:token" => "users#authorize_email" + get "users/authorize-email/:token" => "users_email#confirm" get "users/hp" => "users#get_honeypot_value" get "my/*path", to: 'users#my_redirect' diff --git a/db/migrate/20160307190919_create_email_change_requests.rb b/db/migrate/20160307190919_create_email_change_requests.rb new file mode 100644 index 00000000000..010c3e356d0 --- /dev/null +++ b/db/migrate/20160307190919_create_email_change_requests.rb @@ -0,0 +1,15 @@ +class CreateEmailChangeRequests < ActiveRecord::Migration + def change + create_table :email_change_requests do |t| + t.integer :user_id, null: false + t.string :old_email, length: 513, null: false + t.string :new_email, length: 513, null: false + t.integer :old_email_token_id, null: true + t.integer :new_email_token_id, null: true + t.integer :change_state, null: false + t.timestamps null: false + end + + add_index :email_change_requests, :user_id + end +end diff --git a/db/migrate/20160308193142_rename_confirm_translation_key.rb b/db/migrate/20160308193142_rename_confirm_translation_key.rb new file mode 100644 index 00000000000..b121bf9a943 --- /dev/null +++ b/db/migrate/20160308193142_rename_confirm_translation_key.rb @@ -0,0 +1,8 @@ +class RenameConfirmTranslationKey < ActiveRecord::Migration + def change + execute "UPDATE translation_overrides SET translation_key = 'user_notifications.confirm_new_email.subject_template' + WHERE translation_key = 'user_notifications.authorize_email.subject_template'" + execute "UPDATE translation_overrides SET translation_key = 'user_notifications.confirm_new_email.text_body_template' + WHERE translation_key = 'user_notifications.authorize_email.text_body_template'" + end +end diff --git a/lib/email_updater.rb b/lib/email_updater.rb new file mode 100644 index 00000000000..71c57f05c82 --- /dev/null +++ b/lib/email_updater.rb @@ -0,0 +1,113 @@ +require_dependency 'email' +require_dependency 'has_errors' +require_dependency 'email_validator' + +class EmailUpdater + include HasErrors + + attr_reader :user + + def initialize(guardian=nil, user=nil) + @guardian = guardian + @user = user + end + + def self.human_attribute_name(name, options={}) + User.human_attribute_name(name, options) + end + + def authorize_both? + @user.staff? + end + + def change_to(email_input) + @guardian.ensure_can_edit_email!(@user) + + email = Email.downcase(email_input.strip) + EmailValidator.new(attributes: :email).validate_each(self, :email, email) + + errors.add(:base, I18n.t('change_email.error')) if User.find_by_email(email) + + if errors.blank? + args = { + old_email: @user.email, + new_email: email, + } + + if authorize_both? + args[:change_state] = EmailChangeRequest.states[:authorizing_old] + email_token = @user.email_tokens.create(email: args[:old_email]) + args[:old_email_token] = email_token + else + args[:change_state] = EmailChangeRequest.states[:authorizing_new] + email_token = @user.email_tokens.create(email: args[:new_email]) + args[:new_email_token] = email_token + end + @user.email_change_requests.create(args) + + if args[:change_state] == EmailChangeRequest.states[:authorizing_new] + send_email(:confirm_new_email, email_token) + elsif args[:change_state] == EmailChangeRequest.states[:authorizing_old] + send_email(:confirm_old_email, email_token) + end + end + end + + def confirm(token) + confirm_result = nil + change_req = nil + + User.transaction do + result = EmailToken.atomic_confirm(token) + if result[:success] + token = result[:email_token] + @user = token.user + + change_req = user.email_change_requests + .where('old_email_token_id = :token_id OR new_email_token_id = :token_id', { token_id: token.id}) + .first + + # Simple state machine + case change_req.try(:change_state) + when EmailChangeRequest.states[:authorizing_old] + new_token = user.email_tokens.create(email: change_req.new_email) + change_req.update_columns(change_state: EmailChangeRequest.states[:authorizing_new], + new_email_token_id: new_token.id) + send_email(:confirm_new_email, new_token) + confirm_result = :authorizing_new + when EmailChangeRequest.states[:authorizing_new] + change_req.update_column(:change_state, EmailChangeRequest.states[:complete]) + user.update_column(:email, token.email) + confirm_result = :complete + end + else + errors.add(:base, I18n.t('change_email.already_done')) + confirm_result = :error + end + end + + if confirm_result == :complete && change_req.old_email_token_id.blank? + notify_old(change_req.old_email, token.email) + end + + confirm_result || :error + end + + protected + + def notify_old(old_email, new_email) + Jobs.enqueue :user_email, + to_address: old_email, + type: :notify_old_email, + user_id: @user.id + end + + def send_email(type, email_token) + Jobs.enqueue :user_email, + to_address: email_token.email, + type: type, + user_id: @user.id, + email_token: email_token.token + end + +end diff --git a/spec/components/email_updater_spec.rb b/spec/components/email_updater_spec.rb new file mode 100644 index 00000000000..b868e6c9a57 --- /dev/null +++ b/spec/components/email_updater_spec.rb @@ -0,0 +1,124 @@ +require 'rails_helper' +require_dependency 'email_updater' + +describe EmailUpdater do + let(:old_email) { 'old.email@example.com' } + let(:new_email) { 'new.email@example.com' } + + context 'as a regular user' do + let(:user) { Fabricate(:user, email: old_email) } + let(:updater) { EmailUpdater.new(user.guardian, user) } + + before do + Jobs.expects(:enqueue).once.with(:user_email, has_entries(type: :confirm_new_email, to_address: new_email)) + updater.change_to(new_email) + @change_req = user.email_change_requests.first + end + + it "starts the new confirmation process" do + expect(updater.errors).to be_blank + + expect(@change_req).to be_present + expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new]) + + expect(@change_req.old_email).to eq(old_email) + expect(@change_req.new_email).to eq(new_email) + expect(@change_req.old_email_token).to be_blank + expect(@change_req.new_email_token.email).to eq(new_email) + end + + context 'confirming an invalid token' do + it "produces an error" do + updater.confirm('random') + expect(updater.errors).to be_present + expect(user.reload.email).not_to eq(new_email) + end + end + + context 'confirming a valid token' do + it "updates the user's email" do + Jobs.expects(:enqueue).once.with(:user_email, has_entries(type: :notify_old_email, to_address: old_email)) + updater.confirm(@change_req.new_email_token.token) + expect(updater.errors).to be_blank + expect(user.reload.email).to eq(new_email) + + @change_req.reload + expect(@change_req.change_state).to eq(EmailChangeRequest.states[:complete]) + end + end + + end + + context 'as a staff user' do + let(:user) { Fabricate(:moderator, email: old_email) } + let(:updater) { EmailUpdater.new(user.guardian, user) } + + before do + Jobs.expects(:enqueue).once.with(:user_email, has_entries(type: :confirm_old_email, to_address: old_email)) + updater.change_to(new_email) + @change_req = user.email_change_requests.first + end + + it "starts the old confirmation process" do + expect(updater.errors).to be_blank + + expect(@change_req.old_email).to eq(old_email) + expect(@change_req.new_email).to eq(new_email) + expect(@change_req).to be_present + expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old]) + + expect(@change_req.old_email_token.email).to eq(old_email) + expect(@change_req.new_email_token).to be_blank + end + + context 'confirming an invalid token' do + it "produces an error" do + updater.confirm('random') + expect(updater.errors).to be_present + expect(user.reload.email).not_to eq(new_email) + end + end + + context 'confirming a valid token' do + before do + Jobs.expects(:enqueue).once.with(:user_email, has_entries(type: :confirm_new_email, to_address: new_email)) + updater.confirm(@change_req.old_email_token.token) + @change_req.reload + end + + it "starts the new update process" do + expect(updater.errors).to be_blank + expect(user.reload.email).to eq(old_email) + + expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new]) + expect(@change_req.new_email_token).to be_present + end + + it "cannot be confirmed twice" do + updater.confirm(@change_req.old_email_token.token) + expect(updater.errors).to be_present + expect(user.reload.email).to eq(old_email) + + @change_req.reload + expect(@change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new]) + expect(@change_req.new_email_token.email).to eq(new_email) + end + + context "completing the new update process" do + before do + Jobs.expects(:enqueue).with(:user_email, has_entries(type: :notify_old_email, to_address: old_email)).never + updater.confirm(@change_req.new_email_token.token) + end + + it "updates the user's email" do + expect(updater.errors).to be_blank + expect(user.reload.email).to eq(new_email) + + @change_req.reload + expect(@change_req.change_state).to eq(EmailChangeRequest.states[:complete]) + end + end + end + end +end + diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 4f753041c68..ff0e0d58c4e 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -104,26 +104,6 @@ describe UsersController do end end - describe '.authorize_email' do - it 'errors out for invalid tokens' do - get :authorize_email, token: 'asdfasdf' - expect(response).to be_success - expect(flash[:error]).to be_present - end - - context 'valid token' do - it 'authorizes with a correct token' do - user = Fabricate(:user) - email_token = user.email_tokens.create(email: user.email) - - get :authorize_email, token: email_token.token - expect(response).to be_success - expect(flash[:error]).to be_blank - expect(session[:current_user_id]).to be_present - end - end - end - describe '.activate_account' do before do UsersController.any_instance.stubs(:honeypot_or_challenge_fails?).returns(false) diff --git a/spec/controllers/users_email_controller_spec.rb b/spec/controllers/users_email_controller_spec.rb index a470246a501..0a2bfe99ce3 100644 --- a/spec/controllers/users_email_controller_spec.rb +++ b/spec/controllers/users_email_controller_spec.rb @@ -2,6 +2,44 @@ require 'rails_helper' describe UsersEmailController do + describe '.confirm' do + it 'errors out for invalid tokens' do + get :confirm, token: 'asdfasdf' + expect(response).to be_success + expect(assigns(:update_result)).to eq(:error) + end + + context 'valid old address token' do + let(:user) { Fabricate(:moderator) } + let(:updater) { EmailUpdater.new(user.guardian, user) } + + before do + updater.change_to('new.n.cool@example.com') + end + + it 'confirms with a correct token' do + get :confirm, token: user.email_tokens.last.token + expect(response).to be_success + expect(assigns(:update_result)).to eq(:authorizing_new) + end + end + + context 'valid new address token' do + let(:user) { Fabricate(:user) } + let(:updater) { EmailUpdater.new(user.guardian, user) } + + before do + updater.change_to('new.n.cool@example.com') + end + + it 'confirms with a correct token' do + get :confirm, token: user.email_tokens.last.token + expect(response).to be_success + expect(assigns(:update_result)).to eq(:complete) + end + end + end + describe '.update' do let(:new_email) { 'bubblegum@adventuretime.ooo' } @@ -57,14 +95,8 @@ describe UsersEmailController do end context 'success' do - it 'has an email token' do - expect { xhr :put, :update, username: user.username, email: new_email }.to change(EmailToken, :count) - end - - it 'enqueues an email authorization' do - Jobs.expects(:enqueue).with(:user_email, has_entries(type: :authorize_email, user_id: user.id, to_address: new_email)) - xhr :put, :update, username: user.username, email: new_email + expect { xhr :put, :update, username: user.username, email: new_email }.to change(EmailChangeRequest, :count) end end end diff --git a/spec/jobs/user_email_spec.rb b/spec/jobs/user_email_spec.rb index 5054089b5d0..92fd5abd2a3 100644 --- a/spec/jobs/user_email_spec.rb +++ b/spec/jobs/user_email_spec.rb @@ -38,9 +38,9 @@ describe Jobs::UserEmail do context 'to_address' do it 'overwrites a to_address when present' do - UserNotifications.expects(:authorize_email).returns(mailer) + UserNotifications.expects(:confirm_new_email).returns(mailer) Email::Sender.any_instance.expects(:send) - Jobs::UserEmail.new.execute(type: :authorize_email, user_id: user.id, to_address: 'jake@adventuretime.ooo') + Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id, to_address: 'jake@adventuretime.ooo') expect(mailer.to).to eq(['jake@adventuretime.ooo']) end end diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index a3092909791..ebc9b27e4c5 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -405,7 +405,8 @@ describe UserNotifications do context "user locale has been set" do - %w(signup signup_after_approval authorize_email forgot_password admin_login account_created).each do |mail_type| + %w(signup signup_after_approval confirm_old_email notify_old_email confirm_new_email + forgot_password admin_login account_created).each do |mail_type| include_examples "notification derived from template" do SiteSetting.default_locale = "en" let(:locale) { "fr" } @@ -418,7 +419,8 @@ describe UserNotifications do end context "user locale has not been set" do - %w(signup signup_after_approval authorize_email forgot_password admin_login account_created).each do |mail_type| + %w(signup signup_after_approval notify_old_email confirm_old_email confirm_new_email + forgot_password admin_login account_created).each do |mail_type| include_examples "notification derived from template" do SiteSetting.default_locale = "en" let(:locale) { nil } @@ -431,7 +433,8 @@ describe UserNotifications do end context "user locale is an empty string" do - %w(signup signup_after_approval authorize_email forgot_password admin_login account_created).each do |mail_type| + %w(signup signup_after_approval notify_old_email confirm_new_email confirm_old_email + forgot_password admin_login account_created).each do |mail_type| include_examples "notification derived from template" do SiteSetting.default_locale = "en" let(:locale) { "" }