diff --git a/app/controllers/identity/email_addresses/confirmations_controller.rb b/app/controllers/identity/email_addresses/confirmations_controller.rb deleted file mode 100644 index e19983f82..000000000 --- a/app/controllers/identity/email_addresses/confirmations_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -class Identity::EmailAddresses::ConfirmationsController < ApplicationController - require_untenanted_access - - before_action :set_identity - rate_limit to: 3, within: 1.hour, only: :create - - def show - end - - def create - @identity.change_email_address_using_token(token) - - # Redirect to the tenant that initiated the change - tenant = SignedGlobalID.parse(token, for: Identity::EmailAddressChangeable::EMAIL_CHANGE_TOKEN_PURPOSE)&.params&.fetch("tenant") - - if tenant - redirect_to "#{tenant}/users/#{Current.user.id}/edit" - else - redirect_to session_menu_path - end - rescue ArgumentError => e - flash[:alert] = e.message - render :show, status: :unprocessable_entity - end - - private - def set_identity - @identity = Current.identity - end - - def token - params.expect :email_address_token - end -end diff --git a/app/controllers/identity/email_addresses_controller.rb b/app/controllers/identity/email_addresses_controller.rb deleted file mode 100644 index 147c3bb12..000000000 --- a/app/controllers/identity/email_addresses_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Identity::EmailAddressesController < ApplicationController - require_untenanted_access - - before_action :set_identity - rate_limit to: 3, within: 1.hour, only: :create - - def new - @tenant = params[:tenant] - @user = ApplicationRecord.with_tenant(@tenant) { Current.identity.user } - end - - def create - @tenant = tenant - @user = ApplicationRecord.with_tenant(@tenant) { Current.identity.user } - - if Identity.exists?(email_address: new_email_address) - flash.now[:alert] = "Someone else already uses that email" - render :new, status: :unprocessable_entity - else - @identity.send_email_address_change_confirmation(new_email_address, tenant: tenant) - end - end - - private - def set_identity - @identity = Current.identity - end - - def new_email_address - params.expect :email_address - end - - def tenant - params.expect :tenant - end -end diff --git a/app/controllers/memberships/email_addresses/confirmations_controller.rb b/app/controllers/memberships/email_addresses/confirmations_controller.rb new file mode 100644 index 000000000..962bf78dd --- /dev/null +++ b/app/controllers/memberships/email_addresses/confirmations_controller.rb @@ -0,0 +1,28 @@ +class Memberships::EmailAddresses::ConfirmationsController < ApplicationController + require_untenanted_access + allow_unauthenticated_access + + before_action :set_membership + rate_limit to: 5, within: 1.hour, only: :create + + def show + end + + def create + Membership.change_email_address_using_token(token) + + terminate_session + start_new_session_for @membership.reload.identity + + redirect_to edit_user_url(script_name: "/#{@membership.tenant}", id: @membership.user) + end + + private + def set_membership + @membership = Membership.find(params[:membership_id]) + end + + def token + params.expect :email_address_token + end +end diff --git a/app/controllers/memberships/email_addresses_controller.rb b/app/controllers/memberships/email_addresses_controller.rb new file mode 100644 index 000000000..e918005e1 --- /dev/null +++ b/app/controllers/memberships/email_addresses_controller.rb @@ -0,0 +1,22 @@ +class Memberships::EmailAddressesController < ApplicationController + require_untenanted_access + + before_action :set_membership + rate_limit to: 5, within: 1.hour, only: :create + + def new + end + + def create + @membership.send_email_address_change_confirmation(new_email_address) + end + + private + def set_membership + @membership = Current.identity.memberships.find(params[:membership_id]) + end + + def new_email_address + params.expect :email_address + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 27734a43d..d6db07d61 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -7,6 +7,10 @@ class ApplicationMailer < ActionMailer::Base private def default_url_options - super.merge(script_name: Account.sole.slug) + if ApplicationRecord.current_tenant + super.merge(script_name: Account.sole.slug) + else + super + end end end diff --git a/app/mailers/identity_mailer.rb b/app/mailers/identity_mailer.rb index 3814806d4..58ac5d0b6 100644 --- a/app/mailers/identity_mailer.rb +++ b/app/mailers/identity_mailer.rb @@ -1,9 +1,7 @@ class IdentityMailer < ApplicationMailer - def email_change_confirmation(identity:, email_address:, token:, tenant:) - @identity = identity + def email_change_confirmation(email_address:, token:, membership:) @token = token - @tenant = tenant - + @membership = membership mail to: email_address, subject: "Confirm your new email address" end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 5f024cfa7..70bc089eb 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,5 +1,5 @@ class Identity < UntenantedRecord - include EmailAddressChangeable, Transferable + include Transferable has_many :memberships, dependent: :destroy has_many :magic_links, dependent: :destroy @@ -14,16 +14,6 @@ class Identity < UntenantedRecord end end - def link_to(tenant, context: nil) - memberships.find_or_create_by!(tenant: tenant) do |membership| - membership.context = context - end - end - - def unlink_from(tenant) - memberships.find_by(tenant: tenant)&.destroy - end - def staff? email_address.ends_with?("@37signals.com") || email_address.ends_with?("@basecamp.com") end diff --git a/app/models/identity/email_address_changeable.rb b/app/models/identity/email_address_changeable.rb deleted file mode 100644 index 90e4d94f5..000000000 --- a/app/models/identity/email_address_changeable.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Identity::EmailAddressChangeable - EMAIL_CHANGE_TOKEN_PURPOSE = "change_email_address" - EMAIL_CHANGE_TOKEN_EXPIRATION = 30.minutes - - extend ActiveSupport::Concern - - def send_email_address_change_confirmation(new_email_address, tenant:) - token = generate_email_address_change_token(to: new_email_address, tenant: tenant, expires_in: EMAIL_CHANGE_TOKEN_EXPIRATION) - IdentityMailer.email_change_confirmation(identity: self, email_address: new_email_address, token: token, tenant: tenant).deliver_later - end - - def generate_email_address_change_token(from: email_address, to:, tenant:, **options) - options = options.reverse_merge( - for: EMAIL_CHANGE_TOKEN_PURPOSE, - old_email_address: from, - new_email_address: to, - tenant: tenant - ) - - to_sgid(**options).to_s - end - - def change_email_address_using_token(token) - parsed_token = SignedGlobalID.parse(token, for: EMAIL_CHANGE_TOKEN_PURPOSE) - - if parsed_token.nil? - raise ArgumentError, "The token is invalid" - elsif parsed_token.find != self - raise ArgumentError, "The token is for another identity" - elsif email_address != parsed_token.params.fetch("old_email_address") - raise ArgumentError, "The token was generated for a different email address" - else - tenant = parsed_token.params.fetch("tenant") - new_email_address = parsed_token.params.fetch("new_email_address") - change_email_address(new_email_address, tenant: tenant) - end - end - - private - def change_email_address(new_email_address, tenant:) - old_email_address = email_address - - # First update the identity email - update!(email_address: new_email_address) - - begin - # Then update the membership to point to the new identity - Membership.change_email_address(from: old_email_address, to: new_email_address, tenant: tenant) - - # Also update the user's email in the tenant database (read-only from untenanted context) - # This will be handled by the user updating their own record - rescue => e - # Rollback identity email if membership update fails - update!(email_address: old_email_address) - raise e - end - end -end diff --git a/app/models/membership.rb b/app/models/membership.rb index 56af3e31e..6e909b4ab 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -1,4 +1,6 @@ class Membership < UntenantedRecord + include EmailAddressChangeable + belongs_to :identity, touch: true serialize :context, coder: JSON diff --git a/app/models/membership/email_address_changeable.rb b/app/models/membership/email_address_changeable.rb new file mode 100644 index 000000000..3b6dbdf84 --- /dev/null +++ b/app/models/membership/email_address_changeable.rb @@ -0,0 +1,55 @@ +module Membership::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) + membership = parsed_token&.find + + if parsed_token.nil? + raise ArgumentError, "The token is invalid" + elsif membership.nil? + raise ArgumentError, "The membership no longer exists" + elsif membership.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") + membership.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, + membership: 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 + 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 diff --git a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb index fcdb77cb6..438dc18e9 100644 --- a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb +++ b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb @@ -1,7 +1,7 @@
Hit the button below to use this email address in Fizzy.
-<%= link_to "Yes use use this email address", identity_email_address_confirmation_url(@token), class: "btn" %> +<%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %>If you didn’t request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.
diff --git a/app/views/mailers/identity_mailer/email_change_confirmation.text.erb b/app/views/mailers/identity_mailer/email_change_confirmation.text.erb index bf379f7a3..265c9cbf2 100644 --- a/app/views/mailers/identity_mailer/email_change_confirmation.text.erb +++ b/app/views/mailers/identity_mailer/email_change_confirmation.text.erb @@ -3,6 +3,6 @@ Confirm your email address change Hit the link below to use this email address in Fizzy: -<%= identity_email_address_confirmation_url(@token) %> +<%= email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token) %> If you didn’t request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button. \ No newline at end of file diff --git a/app/views/identity/email_addresses/confirmations/show.html.erb b/app/views/memberships/email_addresses/confirmations/show.html.erb similarity index 77% rename from app/views/identity/email_addresses/confirmations/show.html.erb rename to app/views/memberships/email_addresses/confirmations/show.html.erb index d7c47c5e7..869b64e7e 100644 --- a/app/views/identity/email_addresses/confirmations/show.html.erb +++ b/app/views/memberships/email_addresses/confirmations/show.html.erb @@ -8,7 +8,7 @@Just a sec while we confirm your new email address.
- <%= form_with url: identity_email_address_confirmation_path(params[:email_address_token]), method: :post, data: { controller: "auto-submit" } do |form| %> + <%= form_with url: email_address_confirmation_path(membership_id: @membership.id), method: :post, data: { controller: "auto-submit" } do |form| %> <%= form.hidden_field :email_address_token, value: params[:email_address_token] %>