Reinstate email changes

This commit is contained in:
Stanko K.R.
2025-11-14 08:33:00 +01:00
committed by Mike Dalessio
parent 4d8b3ed719
commit 56c41d45eb
14 changed files with 230 additions and 14 deletions
@@ -12,7 +12,7 @@ class Users::EmailAddresses::ConfirmationsController < ApplicationController
user = User.change_email_address_using_token(token)
terminate_session if Current.session
start_new_session_for user.reload.identity
start_new_session_for user.identity
redirect_to edit_user_url(script_name: user.account.slug, id: user)
end
+7
View File
@@ -0,0 +1,7 @@
class IdentityMailer < ApplicationMailer
def email_change_confirmation(email_address:, token:, user:)
@token = token
@user = user
mail to: email_address, subject: "Confirm your new email address"
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class User < ApplicationRecord
include Accessor, Assignee, Attachable, Configurable,
include Accessor, Assignee, Attachable, Configurable, EmailAddressChangeable,
Mentionable, Named, Notifiable, Role, Searcher, Watcher
include Timelined # Depends on Accessor
@@ -0,0 +1,57 @@
module User::EmailAddressChangeable
EMAIL_CHANGE_TOKEN_PURPOSE = "change_email_address"
EMAIL_CHANGE_TOKEN_EXPIRATION = 30.minutes
extend ActiveSupport::Concern
class_methods do
def change_email_address_using_token(token)
parsed_token = SignedGlobalID.parse(token, for: EMAIL_CHANGE_TOKEN_PURPOSE)
user = parsed_token&.find
if parsed_token.nil?
raise ArgumentError, "The token is invalid"
elsif user.nil?
raise ArgumentError, "The user no longer exists"
elsif user.identity.email_address != parsed_token.params.fetch("old_email_address")
raise ArgumentError, "The token was generated for a different email address"
else
new_email_address = parsed_token.params.fetch("new_email_address")
user.change_email_address(new_email_address)
end
end
end
def send_email_address_change_confirmation(new_email_address)
token = generate_email_address_change_token(
to: new_email_address,
expires_in: EMAIL_CHANGE_TOKEN_EXPIRATION
)
IdentityMailer.email_change_confirmation(
email_address: new_email_address,
token: token,
user: self
).deliver_later
end
def change_email_address(new_email_address)
transaction do
new_identity = Identity.find_or_create_by!(email_address: new_email_address)
update!(identity: new_identity)
end
self
end
private
def generate_email_address_change_token(from: identity.email_address, to:, **options)
options = options.reverse_merge(
for: EMAIL_CHANGE_TOKEN_PURPOSE,
old_email_address: from,
new_email_address: to,
)
to_sgid(**options).to_s
end
end
@@ -1,6 +1,6 @@
<p class="subtitle">Confirm your email address change</p>
<%= link_to "Yes use use this email address", email_address_confirmation_url(user_id: @user.id, email_address_token: @token), class: "btn" %>
<%= link_to "Yes use use this email address", user_email_address_confirmation_url(user_id: @user.id, email_address_token: @token), class: "btn" %>
<p class="margin-block-start-double">If you didnt request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.</p>
@@ -3,6 +3,6 @@ Confirm your email address change
Hit the link below to use this email address in Fizzy:
<%= email_address_confirmation_url(user_id: @user.id, email_address_token: @token) %>
<%= user_email_address_confirmation_url(user_id: @user.id, email_address_token: @token) %>
If you didnt request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.
@@ -8,7 +8,7 @@
<p class="margin-none">Just a sec while we confirm your new email address.</p>
</header>
<%= form_with url: email_address_confirmation_path(user_id: @user.id), method: :post, data: { controller: "form auto-submit" } do |form| %>
<%= form_with url: user_email_address_confirmation_path(user_id: @user.id), method: :post, data: { controller: "form auto-submit" } do |form| %>
<%= form.hidden_field :email_address_token, value: params[:email_address_token] %>
<button type="submit" class="btn btn--link center" data-form-target="submit">
@@ -5,7 +5,7 @@ class Sessions::MenusControllerTest < ActionDispatch::IntegrationTest
@identity = identities(:kevin)
end
test "show with no memberships" do
test "show with no account" do
sign_in_as @identity
@identity.users.delete_all
@@ -16,7 +16,7 @@ class Sessions::MenusControllerTest < ActionDispatch::IntegrationTest
assert_response :success, "Renders an empty menu"
end
test "show with exactly one membership" do
test "show with exactly one account" do
sign_in_as @identity
@identity.users.delete_all
account = Account.create!(external_account_id: 9999991, name: "Test Account")
@@ -30,7 +30,7 @@ class Sessions::MenusControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to root_url(script_name: "/9999991")
end
test "show with multiple memeberships" do
test "show with multiple accounts" do
sign_in_as @identity
@identity.users.delete_all
account1 = Account.create!(external_account_id: 9999992, name: "37signals")
@@ -0,0 +1,35 @@
require "test_helper"
class Users::EmailAddresses::ConfirmationsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:david)
@new_email = "newemail@example.com"
@token = @user.send(:generate_email_address_change_token, to: @new_email)
end
test "show" do
untenanted do
get user_email_address_confirmation_path(user_id: @user.id, email_address_token: @token)
assert_response :success
end
end
test "create" do
old_email = @user.identity.email_address
untenanted do
post user_email_address_confirmation_path(user_id: @user.id, email_address_token: @token)
assert_equal @new_email, @user.reload.identity.email_address
assert_redirected_to edit_user_url(script_name: @user.account.slug, id: @user)
end
end
test "create with invalid token" do
untenanted do
assert_raises(ArgumentError) do
post user_email_address_confirmation_path(user_id: @user.id, email_address_token: "invalid")
end
end
end
end
@@ -0,0 +1,48 @@
require "test_helper"
class Users::EmailAddressesControllerTest < ActionDispatch::IntegrationTest
include ActionMailer::TestHelper
setup do
sign_in_as :david
@user = users(:david)
end
test "new" do
untenanted do
get new_user_email_address_path(@user)
assert_response :success
end
end
test "create" do
untenanted do
assert_emails 1 do
post user_email_addresses_path(@user), params: { email_address: "newemail@example.com" }
end
assert_response :success
end
end
test "create with existing email in same account" do
existing_user = users(:kevin)
existing_email = existing_user.identity.email_address
untenanted do
post user_email_addresses_path(@user), params: { email_address: existing_email }
assert_redirected_to new_user_email_address_path(@user)
assert_equal "You already have a user in this account with that email address", flash[:alert]
end
end
test "create for other user" do
other_user = users(:kevin)
untenanted do
assert_no_emails do
post user_email_addresses_path(other_user), params: { email_address: "newemail@example.com" }
end
assert_response :not_found
end
end
end
+4 -4
View File
@@ -18,13 +18,13 @@ class ApplicationHelperTest < ActionView::TestCase
assert_select parse(page_title_tag), "title", text: "Holodeck | Fizzy"
end
test "page_title_tag on tenanted page when user has a single membership" do
test "page_title_tag on tenanted page when user has a single account" do
Current.session = sessions(:david)
assert_select parse(page_title_tag), "title", text: "Fizzy"
end
test "page_title_tag on tenanted page when user has multiple memberships" do
test "page_title_tag on tenanted page when user has multiple accounts" do
Current.session = sessions(:david)
other_account = Account.create!(external_account_id: "dangling-tenant", name: "Other Account")
identities(:david).users.create!(account: other_account, name: "David")
@@ -32,14 +32,14 @@ class ApplicationHelperTest < ActionView::TestCase
assert_select parse(page_title_tag), "title", text: "37signals | Fizzy"
end
test "page_title_tag on tenanted page with a page title when user has a single membership" do
test "page_title_tag on tenanted page with a page title when user has a single account" do
Current.session = sessions(:david)
@page_title = "Holodeck"
assert_select parse(page_title_tag), "title", text: "Holodeck | Fizzy"
end
test "page_title_tag on tenanted page with a page title when user has multiple memberships" do
test "page_title_tag on tenanted page with a page title when user has multiple account" do
Current.session = sessions(:david)
other_account = Account.create!(external_account_id: "dangling-tenant", name: "Other Account")
identities(:david).users.create!(account: other_account, name: "David")
@@ -0,0 +1,13 @@
class IdentityMailerPreview < ActionMailer::Preview
def email_change_confirmation
new_email_address = "new.email@example.com"
user = User.all.sample
token = user.send(:generate_email_address_change_token, to: new_email_address)
IdentityMailer.email_change_confirmation(
email_address: new_email_address,
token: token,
user: user
)
end
end
+3 -2
View File
@@ -19,7 +19,7 @@ class IdentityTest < ActiveSupport::TestCase
assert_not Identity.new(email_address: "test@example.com").staff?
end
test "join creates membership and user for account" do
test "join" do
identity = identities(:david)
account = accounts(:initech)
@@ -27,7 +27,8 @@ class IdentityTest < ActiveSupport::TestCase
identity.join(account)
end
user = account.users.find_by(identity: identity)
user = account.users.find_by!(identity: identity)
assert_not_nil user
assert_equal identity, user.identity
assert_equal identity.email_address, user.name
@@ -0,0 +1,55 @@
require "test_helper"
class User::EmailAddressChangeableTest < ActiveSupport::TestCase
include ActionMailer::TestHelper
setup do
@identity = identities(:kevin)
@user = @identity.users.find_by!(account: accounts("37s"))
@new_email = "newart@example.com"
end
test "send_email_address_change_confirmation" do
assert_emails 1 do
@user.send_email_address_change_confirmation(@new_email)
end
end
test "change_email_address" do
old_identity = @identity
assert_difference -> { Identity.count }, +1 do
@user.change_email_address(@new_email)
end
assert_equal @new_email, @user.reload.identity.email_address
assert_not old_identity.reload.users.exists?(id: @user.id)
assert_equal @new_email, @user.reload.identity.email_address
assert_no_difference -> { Identity.count } do
@user.change_email_address(identities(:david).email_address)
end
assert_equal identities(:david).email_address, @user.reload.identity.email_address
end
test "change_email_address_using_token" do
token = @user.send(:generate_email_address_change_token, to: @new_email)
User.change_email_address_using_token(token)
assert_equal @new_email, @user.reload.identity.email_address
end
test "change_email_address_using_token with invalid token" do
assert_raises(ArgumentError, match: /invalid/) do
User.change_email_address_using_token("invalid_token")
end
token = @user.send(:generate_email_address_change_token, to: @new_email)
@identity.update!(email_address: "different@example.com")
assert_raises(ArgumentError, match: /different email address/) do
User.change_email_address_using_token(token)
end
end
end