diff options
author | Sean McGivern <sean@mcgivern.me.uk> | 2017-10-05 12:12:30 +0000 |
---|---|---|
committer | Sean McGivern <sean@mcgivern.me.uk> | 2017-10-05 12:12:30 +0000 |
commit | 1fe7a42afca83fbbda638d9569d58a87039cd8f2 (patch) | |
tree | 507402412502a3faae74ad3be29f31ed2f7da366 /app | |
parent | 3594a67d30f26247e0234784c4cea7a238c5e417 (diff) | |
parent | 782c017ca0c85311373d53d49d3b9af642b6a85d (diff) | |
download | gitlab-ce-1fe7a42afca83fbbda638d9569d58a87039cd8f2.tar.gz |
Merge branch 'digitalmoksha/gitlab-ce-feature/verify_secondary_emails' into 'master'
Send a confirmation email when the user adds a secondary email address
Closes #37385, #28621, and #36959
See merge request gitlab-org/gitlab-ce!14703
Diffstat (limited to 'app')
23 files changed, 149 insertions, 96 deletions
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 67abe6e88ed..6c521bb06ee 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -392,11 +392,11 @@ table.u2f-registrations { } } -.gpg-email-badge { +.email-badge { display: inline; margin-right: $gl-padding / 2; - .gpg-email-badge-email { + .email-badge-email { display: inline; margin-right: $gl-padding / 4; } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 676a7203c7d..156a8e2c515 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController def remove_email email = user.emails.find(params[:email_id]) - success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute + success = Emails::DestroyService.new(current_user, user: user).execute(email) respond_to do |format| if success diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 0c2646d7bf0..80ab681ed87 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -10,13 +10,14 @@ class ConfirmationsController < Devise::ConfirmationsController users_almost_there_path end - def after_confirmation_path_for(resource_name, resource) - if signed_in?(resource_name) + def after_confirmation_path_for(_resource_name, resource) + # incoming resource can either be a :user or an :email + if signed_in?(:user) after_sign_in(resource) else Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") flash[:notice] += " Please sign in." - new_session_path(resource_name) + new_session_path(:user) end end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 97db84b92d4..bbd7ba49d77 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -1,15 +1,14 @@ class Profiles::EmailsController < Profiles::ApplicationController + before_action :find_email, only: [:destroy, :resend_confirmation_instructions] + def index - @primary = current_user.email + @primary_email = current_user.email @emails = current_user.emails.order_id_desc end def create @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute - - if @email.errors.blank? - NotificationService.new.new_email(@email) - else + unless @email.errors.blank? flash[:alert] = @email.errors.full_messages.first end @@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController end def destroy - @email = current_user.emails.find(params[:id]) - - Emails::DestroyService.new(current_user, user: current_user, email: @email.email).execute + Emails::DestroyService.new(current_user, user: current_user).execute(@email) respond_to do |format| format.html { redirect_to profile_emails_url, status: 302 } @@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController end end + def resend_confirmation_instructions + if Emails::ConfirmService.new(current_user, user: current_user).execute(@email) + flash[:notice] = "Confirmation email sent to #{@email.email}" + else + flash[:alert] = "There was a problem sending the confirmation email" + end + + redirect_to profile_emails_url + end + private def email_params params.require(:email).permit(:email) end + + def find_email + @email = current_user.emails.find(params[:id]) + end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index c401030e34a..4f5edeb9bda 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -7,12 +7,6 @@ module Emails mail(to: @user.notification_email, subject: subject("Account was created for you")) end - def new_email_email(email_id) - @email = Email.find(email_id) - @current_user = @user = @email.user - mail(to: @user.notification_email, subject: subject("Email was added to your account")) - end - def new_ssh_key_email(key_id) @key = Key.find_by(id: key_id) diff --git a/app/models/email.rb b/app/models/email.rb index 826d4f16edb..384f38f2db7 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -7,6 +7,13 @@ class Email < ActiveRecord::Base validates :email, presence: true, uniqueness: true, email: true validate :unique_email, if: ->(email) { email.email_changed? } + scope :confirmed, -> { where.not(confirmed_at: nil) } + + after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') } + + devise :confirmable + self.reconfirmable = false # currently email can't be changed, no need to reconfirm + def email=(value) write_attribute(:email, value.downcase.strip) end @@ -14,4 +21,9 @@ class Email < ActiveRecord::Base def unique_email self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) end + + # once email is confirmed, update the gpg signatures + def update_invalid_gpg_signatures + user.update_invalid_gpg_signatures if confirmed? + end end diff --git a/app/models/user.rb b/app/models/user.rb index 4e71a3e11c2..4ba9130a75a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -163,15 +163,16 @@ class User < ActiveRecord::Base before_validation :sanitize_attrs before_validation :set_notification_email, if: :email_changed? before_validation :set_public_email, if: :public_email_changed? - - after_update :update_emails_with_primary_email, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: :external_changed? before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } + before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } after_save :ensure_namespace_correct + after_destroy :post_destroy_hook + after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') } after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } + after_initialize :set_projects_limit - after_destroy :post_destroy_hook # User's Layout preference enum layout: [:fixed, :fluid] @@ -525,12 +526,24 @@ class User < ActiveRecord::Base errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) end + # see if the new email is already a verified secondary email + def check_for_verified_email + skip_reconfirmation! if emails.confirmed.where(email: self.email).any? + end + + # Note: the use of the Emails services will cause `saves` on the user object, running + # through the callbacks again and can have side effects, such as the `previous_changes` + # hash and `_was` variables getting munged. + # By using an `after_commit` instead of `after_update`, we avoid the recursive callback + # scenario, though it then requires us to use the `previous_changes` hash def update_emails_with_primary_email + previous_email = previous_changes[:email][0] # grab this before the DestroyService is called primary_email_record = emails.find_by(email: email) - if primary_email_record - Emails::DestroyService.new(self, user: self, email: email).execute - Emails::CreateService.new(self, user: self, email: email_was).execute - end + Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record + + # the original primary email was confirmed, and we want that to carry over. We don't + # have access to the original confirmation values at this point, so just set confirmed_at + Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at) end def update_invalid_gpg_signatures @@ -816,6 +829,10 @@ class User < ActiveRecord::Base avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username) end + def primary_email_verified? + confirmed? && !temp_oauth_email? + end + def all_emails all_emails = [] all_emails << email unless temp_oauth_email? @@ -823,6 +840,18 @@ class User < ActiveRecord::Base all_emails end + def verified_emails + verified_emails = [] + verified_emails << email if primary_email_verified? + verified_emails.concat(emails.confirmed.pluck(:email)) + verified_emails + end + + def verified_email?(check_email) + downcased = check_email.downcase + email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists? + end + def hook_attrs { name: name, @@ -1047,10 +1076,6 @@ class User < ActiveRecord::Base ensure_rss_token! end - def verified_email?(email) - self.email == email - end - def sync_attribute?(attribute) return true if ldap_user? && attribute == :email diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb index 7f591c89411..5bbceeb3b3f 100644 --- a/app/services/emails/base_service.rb +++ b/app/services/emails/base_service.rb @@ -1,9 +1,8 @@ module Emails class BaseService - def initialize(current_user, opts) - @current_user = current_user - @user = opts.delete(:user) - @email = opts[:email] + def initialize(current_user, params = {}) + @current_user, @params = current_user, params.dup + @user = params.delete(:user) end end end diff --git a/app/services/emails/confirm_service.rb b/app/services/emails/confirm_service.rb new file mode 100644 index 00000000000..b5301bf2b82 --- /dev/null +++ b/app/services/emails/confirm_service.rb @@ -0,0 +1,7 @@ +module Emails + class ConfirmService < ::Emails::BaseService + def execute(email) + email.resend_confirmation_instructions + end + end +end diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb index b6491ee9804..94a841af7c3 100644 --- a/app/services/emails/create_service.rb +++ b/app/services/emails/create_service.rb @@ -1,7 +1,7 @@ module Emails class CreateService < ::Emails::BaseService - def execute - @user.emails.create(email: @email) + def execute(extra_params = {}) + @user.emails.create(@params.merge(extra_params)) end end end diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb index 44011cc36c8..1ed131fe326 100644 --- a/app/services/emails/destroy_service.rb +++ b/app/services/emails/destroy_service.rb @@ -1,7 +1,7 @@ module Emails class DestroyService < ::Emails::BaseService - def execute - update_secondary_emails! if Email.find_by_email!(@email).destroy + def execute(email) + email.destroy && update_secondary_emails! end private diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e2a80db06a6..8d5da459882 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -31,13 +31,6 @@ class NotificationService end end - # Always notify user about email added to profile - def new_email(email) - if email.user&.can?(:receive_notifications) - mailer.new_email_email(email.id).deliver_later - end - end - # When create an issue we should send an email to: # # * issue assignee if their notification level is not Disabled diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml new file mode 100644 index 00000000000..65565b7b8a8 --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml @@ -0,0 +1,16 @@ +- confirmation_link = confirmation_url(@resource, confirmation_token: @token) +- if @resource.unconfirmed_email.present? + #content + = email_default_heading(@resource.unconfirmed_email) + %p Click the link below to confirm your email address. + #cta + = link_to 'Confirm your email address', confirmation_link +- else + #content + - if Gitlab.com? + = email_default_heading('Thanks for signing up to GitLab!') + - else + = email_default_heading("Welcome, #{@resource.name}!") + %p To get started, click the link below to confirm your account. + #cta + = link_to 'Confirm your account', confirmation_link diff --git a/app/views/devise/mailer/_confirmation_instructions_account.text.erb b/app/views/devise/mailer/_confirmation_instructions_account.text.erb new file mode 100644 index 00000000000..01f09aa763d --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_account.text.erb @@ -0,0 +1,14 @@ +<% if @resource.unconfirmed_email.present? %> +<%= @resource.unconfirmed_email %>, + +Use the link below to confirm your email address. +<% else %> + <% if Gitlab.com? %> +Thanks for signing up to GitLab! + <% else %> +Welcome, <%= @resource.name %>! + <% end %> +To get started, use the link below to confirm your account. +<% end %> + +<%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml new file mode 100644 index 00000000000..3d0a1f622a5 --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml @@ -0,0 +1,8 @@ +#content + = email_default_heading("#{@resource.user.name}, you've added an additional email!") + %p Click the link below to confirm your email address (#{@resource.email}) + #cta + = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) + %p + If this email was added in error, you can remove it here: + = link_to "Emails", profile_emails_url diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb new file mode 100644 index 00000000000..a3b28cb0b84 --- /dev/null +++ b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb @@ -0,0 +1,7 @@ +<%= @resource.user.name %>, you've added an additional email! + +Use the link below to confirm your email address (<%= @resource.email %>) + +<%= confirmation_url(@resource, confirmation_token: @token) %> + +If this email was added in error, you can remove it here: <%= profile_emails_url %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml index 4d1037807be..50ee7b53d8f 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.haml +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -1,16 +1 @@ -- confirmation_link = confirmation_url(@resource, confirmation_token: @token) -- if @resource.unconfirmed_email.present? - #content - = email_default_heading(@resource.unconfirmed_email) - %p Click the link below to confirm your email address. - #cta - = link_to confirmation_link, confirmation_link -- else - #content - - if Gitlab.com? - = email_default_heading('Thanks for signing up to GitLab!') - - else - = email_default_heading("Welcome, #{@resource.name}!") - %p To get started, click the link below to confirm your account. - #cta - = link_to confirmation_link, confirmation_link += render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}" diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb index 9f76edb76a4..05fddddf415 100644 --- a/app/views/devise/mailer/confirmation_instructions.text.erb +++ b/app/views/devise/mailer/confirmation_instructions.text.erb @@ -1,9 +1 @@ -Welcome, <%= @resource.name %>! - -<% if @resource.unconfirmed_email.present? %> -You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below: -<% else %> -You can confirm your account through the link below: -<% end %> - -<%= confirmation_url(@resource, confirmation_token: @token) %> +<%= render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}" %>
\ No newline at end of file diff --git a/app/views/notify/new_email_email.html.haml b/app/views/notify/new_email_email.html.haml deleted file mode 100644 index 4a0448a573c..00000000000 --- a/app/views/notify/new_email_email.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%p - Hi #{@user.name}! -%p - A new email was added to your account: -%p - email: - %code= @email.email -%p - If this email was added in error, you can remove it here: - = link_to "Emails", profile_emails_url diff --git a/app/views/notify/new_email_email.text.erb b/app/views/notify/new_email_email.text.erb deleted file mode 100644 index 51cba99ad0d..00000000000 --- a/app/views/notify/new_email_email.text.erb +++ /dev/null @@ -1,7 +0,0 @@ -Hi <%= @user.name %>! - -A new email was added to your account: - -email.................. <%= @email.email %> - -If this email was added in error, you can remove it here: <%= profile_emails_url %> diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 612ecbbb96a..df1df4f5d72 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -32,19 +32,25 @@ All email addresses will be used to identify your commits. %ul.well-list %li - = @primary + = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? } %span.pull-right %span.label.label-success Primary email - - if @primary === current_user.public_email + - if @primary_email === current_user.public_email %span.label.label-info Public email - - if @primary === current_user.notification_email + - if @primary_email === current_user.notification_email %span.label.label-info Notification email - @emails.each do |email| %li - = email.email + = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? } %span.pull-right - if email.email === current_user.public_email %span.label.label-info Public email - if email.email === current_user.notification_email %span.label.label-info Notification email - = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-warning prepend-left-10' + - unless email.confirmed? + - confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email" + = link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10' + + = link_to profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-danger prepend-left-10' do + %span.sr-only Remove + = icon('trash') diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index b04981f90e3..970e92aadaa 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -3,7 +3,7 @@ = icon 'key', class: "settings-list-icon hidden-xs" .key-list-item-info - key.emails_with_verified_status.map do |email, verified| - = render partial: 'email_with_badge', locals: { email: email, verified: verified } + = render partial: 'shared/email_with_badge', locals: { email: email, verified: verified } .description %code= key.fingerprint diff --git a/app/views/profiles/gpg_keys/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml index 5f7844584e1..b7bbc109238 100644 --- a/app/views/profiles/gpg_keys/_email_with_badge.html.haml +++ b/app/views/shared/_email_with_badge.html.haml @@ -2,7 +2,7 @@ - css_classes << (verified ? 'verified': 'unverified') - text = verified ? 'Verified' : 'Unverified' -.gpg-email-badge - .gpg-email-badge-email= email +.email-badge + .email-badge-email= email %div{ class: css_classes } = text |