Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/controllers/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ def update_notifications
end
end

def merge
@legacy_user = User.find_by(id: session[:pending_merge_user_id])
@provider = session[:pending_merge_provider]

unless @legacy_user && @provider
redirect_to edit_profile_path
end
end

def confirm_merge
legacy_user = User.find_by(id: session[:pending_merge_user_id])
provider = session[:pending_merge_provider]
uid = session[:pending_merge_uid]

unless legacy_user && provider && uid
flash[:danger] = 'Merge session expired. Please try again.'
redirect_to edit_profile_path and return
end

current_user.merge_from!(legacy_user)
current_user.identities.create!(provider: provider, uid: uid)

session.delete(:pending_merge_user_id)
session.delete(:pending_merge_provider)
session.delete(:pending_merge_uid)

flash[:info] = "Successfully merged accounts and connected #{Identity::PROVIDER_NAMES[provider]}."
redirect_to edit_profile_path
end

private

def user_params
Expand Down
10 changes: 8 additions & 2 deletions app/controllers/users/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,18 @@ def link_identity_to_current_user
else
flash[:danger] = "This #{provider_name} account is already connected to another user."
end
redirect_to edit_profile_path
elsif (legacy_user = User.find_by(provider: auth_hash.provider, uid: auth_hash.uid))
# Found a legacy user - need to merge
session[:pending_merge_user_id] = legacy_user.id
session[:pending_merge_provider] = auth_hash.provider
session[:pending_merge_uid] = auth_hash.uid
redirect_to merge_profile_path
else
current_user.identities.create!(provider: auth_hash.provider, uid: auth_hash.uid)
flash[:info] = "Successfully connected #{provider_name} to your account."
redirect_to edit_profile_path
end

redirect_to edit_profile_path
end

def authenticate_with_hash(user = nil)
Expand Down
15 changes: 15 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ def profile_errors
end
profile_errors
end

# Merge another user's associations into this user
def merge_from!(other_user)
transaction do
other_user.invitations.update_all(user_id: id)
other_user.teammates.update_all(user_id: id)
other_user.speakers.update_all(user_id: id)
other_user.ratings.update_all(user_id: id)
other_user.comments.update_all(user_id: id)
other_user.notifications.update_all(user_id: id)

# Clear legacy OAuth fields so the old user can't be found via from_omniauth
other_user.update_columns(provider: nil, uid: nil)
end
end
end

# == Schema Information
Expand Down
39 changes: 39 additions & 0 deletions app/views/profiles/merge.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.row
.col-md-12
%header.page-header
%h1 Merge Accounts

.row
.col-md-8
.widget
.widget-header
%i.bi.bi-exclamation-triangle
%h3 Account Merge Required
.widget-content
%p
The #{Identity::PROVIDER_NAMES[@provider]} account you're trying to connect is already linked to another account.
To connect it to your current account, we need to merge the two accounts.

%h4 What will be merged:
%ul
- if @legacy_user.speakers.any?
%li
%strong= pluralize(@legacy_user.speakers.count, 'proposal')
- if @legacy_user.teammates.any?
%li
%strong= pluralize(@legacy_user.teammates.count, 'team membership')
- if @legacy_user.ratings.any?
%li
%strong= pluralize(@legacy_user.ratings.count, 'rating')
- if @legacy_user.comments.any?
%li
%strong= pluralize(@legacy_user.comments.count, 'comment')
- if @legacy_user.speakers.empty? && @legacy_user.teammates.empty? && @legacy_user.ratings.empty? && @legacy_user.comments.empty?
%li No proposals, team memberships, or other data to merge.

%p.text-muted
The old account's profile information (name, email, bio) will not be transferred.

.mt-4
= button_to 'Merge Accounts', merge_profile_path, class: 'btn btn-primary', data: {turbo_confirm: 'Are you sure you want to merge these accounts? This cannot be undone.'}
= link_to 'Cancel', edit_profile_path, class: 'btn btn-secondary ms-2'
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
resource :profile, only: [:show, :edit, :update] do
get :notifications
patch :notifications, action: :update_notifications
get :merge
post :merge, action: :confirm_merge
end
get '/my-proposals' => 'proposals#index', as: :proposals

Expand Down
15 changes: 15 additions & 0 deletions spec/controllers/users/omniauth_callbacks_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,20 @@
expect(flash[:danger]).to eq('This GitHub account is already connected to another user.')
end
end

context 'when legacy user exists with same provider/uid' do
it 'redirects to merge page' do
legacy_user = create(:user, provider: 'github', uid: github_auth_hash.uid)
user = create(:user)
sign_in(user)

get :github

expect(response).to redirect_to(merge_profile_path)
expect(session[:pending_merge_user_id]).to eq(legacy_user.id)
expect(session[:pending_merge_provider]).to eq('github')
expect(session[:pending_merge_uid]).to eq(github_auth_hash.uid)
end
end
end
end
29 changes: 29 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,33 @@
expect(user.profile_errors.messages[:email][0]).to eq unconfirmed_email_err_msg
end
end

describe '#merge_from!' do
let(:current_user) { create(:user) }
let(:legacy_user) { create(:user, provider: 'twitter', uid: '12345') }
let(:event) { create(:event) }
let(:proposal) { create(:proposal, event: event) }

it 'transfers all associations and clears legacy OAuth fields' do
invitation = create(:invitation, user: legacy_user, email: '[email protected]', proposal: proposal)
teammate = create(:teammate, user: legacy_user, event: event, role: 'reviewer')
speaker = create(:speaker, user: legacy_user, proposal: proposal)
rating = create(:rating, user: legacy_user, proposal: proposal)
comment = create(:comment, user: legacy_user, proposal: proposal)
notification = create(:notification, user: legacy_user)

current_user.merge_from!(legacy_user)

expect(invitation.reload.user_id).to eq(current_user.id)
expect(teammate.reload.user_id).to eq(current_user.id)
expect(speaker.reload.user_id).to eq(current_user.id)
expect(rating.reload.user_id).to eq(current_user.id)
expect(comment.reload.user_id).to eq(current_user.id)
expect(notification.reload.user_id).to eq(current_user.id)

legacy_user.reload
expect(legacy_user.provider).to be_nil
expect(legacy_user.uid).to be_nil
end
end
end