diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index 3e0744b50..85df5152b 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -78,7 +78,7 @@ def maybe_update_user(user, data) user&.update(given_name: data.given_name, family_name: data.family_name) end - def update_email(csp_user, new_emails) + def update_email(csp_user, new_emails, primary_email) return unless csp_user existing_emails = csp_user.user_emails @@ -87,6 +87,10 @@ def update_email(csp_user, new_emails) ActiveRecord::Base.transaction do add_or_activate_new_email(csp_user, new_emails, existing_emails) deactivate_old_email(new_emails, existing_emails) + + # Set their primary email, which should now exist in the user_emails table + primary_email = csp_user.user_emails.find_by(email: primary_email) + primary_email.update(primary: true) if primary_email && !primary_email.primary? end end @@ -154,7 +158,15 @@ def csp def post_signin_actions(user, csp_user, auth) ial_2_actions(user, auth) - update_email(csp_user, auth.extra.raw_info.all_emails) + update_email(csp_user, all_emails(auth), primary_email(auth)) + end + + def primary_email(auth) + auth.info.email + end + + def all_emails(auth) + auth.extra.raw_info.all_emails end end # rubocop:enable Metrics/ClassLength diff --git a/dpc-portal/app/models/user_email.rb b/dpc-portal/app/models/user_email.rb index afc006aed..80476f91c 100644 --- a/dpc-portal/app/models/user_email.rb +++ b/dpc-portal/app/models/user_email.rb @@ -4,4 +4,17 @@ class UserEmail < ApplicationRecord belongs_to :csp_user has_one :user, through: :csp_users + + # If we update this email to primary, make sure the others aren't. + before_save :ensure_only_one_primary, if: -> { primary? && primary_changed? } + + private + + # Sets all of the user's other emails to not primary. + def ensure_only_one_primary + UserEmail.where(csp_user_id: csp_user_id) + .where(primary: true) + .where.not(id: id) + .update_all(primary: false) + end end diff --git a/dpc-portal/db/migrate/20260527181236_add_primary_to_user_emails.rb b/dpc-portal/db/migrate/20260527181236_add_primary_to_user_emails.rb new file mode 100644 index 000000000..43d013287 --- /dev/null +++ b/dpc-portal/db/migrate/20260527181236_add_primary_to_user_emails.rb @@ -0,0 +1,11 @@ +class AddPrimaryToUserEmails < ActiveRecord::Migration[8.0] + def change + add_column :user_emails, :primary, :boolean, default: false, null: false + + # Make sure each csp_user can only have one primary email. + add_index :user_emails, :csp_user_id, + unique: true, + where: '"primary" = true', + name: 'index_unique_primary_email_per_csp_user' + end +end diff --git a/dpc-portal/db/schema.rb b/dpc-portal/db/schema.rb index 543bcd729..03044bce5 100644 --- a/dpc-portal/db/schema.rb +++ b/dpc-portal/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_04_24_194005) do +ActiveRecord::Schema[8.0].define(version: 2026_05_27_181236) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -252,7 +252,9 @@ t.datetime "reactivated_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "primary", default: false, null: false t.index ["csp_user_id", "email"], name: "index_user_emails_on_csp_user_id_and_email", unique: true + t.index ["csp_user_id"], name: "index_unique_primary_email_per_csp_user", unique: true, where: "(\"primary\" = true)" t.index ["csp_user_id"], name: "index_user_emails_on_csp_user_id" end diff --git a/dpc-portal/spec/models/user_email_spec.rb b/dpc-portal/spec/models/user_email_spec.rb index 63fb237f5..a20a11769 100644 --- a/dpc-portal/spec/models/user_email_spec.rb +++ b/dpc-portal/spec/models/user_email_spec.rb @@ -10,4 +10,35 @@ user_email = create(:user_email, csp_user:) expect(user_email.csp_user).to eq csp_user end + + it 'ensures that creating a new primary email unsets others' do + user = create(:user) + csp = create(:csp) + csp_user = create(:csp_user, user:, csp:) + + email_old_primary = create(:user_email, csp_user:, primary: true, email: 'old@email.com') + email_new_primary = create(:user_email, csp_user:, primary: true, email: 'new@email.com') + + expect(email_old_primary.reload.primary).to eq false + expect(email_new_primary.reload.primary).to eq true + end + + it 'ensures that setting an email to primary unsets others' do + user = create(:user) + csp = create(:csp) + csp_user = create(:csp_user, user:, csp:) + + email_old_primary = create(:user_email, csp_user:, primary: true, email: 'old@email.com') + email_new_primary = create(:user_email, csp_user:, primary: false, email: 'new@email.com') + expect(email_old_primary.primary).to eq true + expect(email_new_primary.primary).to eq false + + # Should set email1 to not primary + email_new_primary.update!(primary: true) + email_old_primary.reload + email_new_primary.reload + + expect(email_old_primary.primary).to eq false + expect(email_new_primary.primary).to eq true + end end diff --git a/dpc-portal/spec/requests/login_dot_gov_spec.rb b/dpc-portal/spec/requests/login_dot_gov_spec.rb index b67b4d6c2..f226140e5 100644 --- a/dpc-portal/spec/requests/login_dot_gov_spec.rb +++ b/dpc-portal/spec/requests/login_dot_gov_spec.rb @@ -209,6 +209,8 @@ emails = UserEmail.last(2).pluck(:email) expect(emails).to match_array(%w[email1@example.com email2@example.com]) expect(UserEmail.pluck(:active)).to all(be true) + expect(UserEmail.find(&:primary?).email).to eq 'email1@example.com' + expect(UserEmail.count(&:primary?)).to eq 1 end end @@ -270,6 +272,7 @@ expect(email.active).to eq true expect(email.deactivated_at).to be_nil expect(email.reactivated_at).to_not be_nil + expect(email.primary).to eq true end end end