FIX: Invite redemption error if user had already redeemed (#19070)

When opening the invite acceptance page when the user
was already logged in, we were still showing the Accept
Invitation prompt even if the user had already redeemed
the invitation and was present in the `InvitedUser` table.

This would lead to errors when the user clicked on the button.

This commit fixes the issue by hiding the Accept Invitation
button and showing an error message instead indicating that
the user had already redeemed the invitation. This only applies
to multi-use invite links.
This commit is contained in:
Martin Brennan 2022-11-17 15:51:58 +10:00 committed by GitHub
parent 5ee3e2932f
commit 40e8912395
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 38 additions and 3 deletions

View File

@ -31,6 +31,9 @@ export default Controller.extend(
accountEmail: alias("email"), accountEmail: alias("email"),
existingUserId: readOnly("model.existing_user_id"), existingUserId: readOnly("model.existing_user_id"),
existingUserCanRedeem: readOnly("model.existing_user_can_redeem"), existingUserCanRedeem: readOnly("model.existing_user_can_redeem"),
existingUserCanRedeemError: readOnly(
"model.existing_user_can_redeem_error"
),
existingUserRedeeming: bool("existingUserId"), existingUserRedeeming: bool("existingUserId"),
hiddenEmail: alias("model.hidden_email"), hiddenEmail: alias("model.hidden_email"),
emailVerifiedByLink: alias("model.email_verified_by_link"), emailVerifiedByLink: alias("model.email_verified_by_link"),

View File

@ -135,7 +135,7 @@
{{#if this.existingUserCanRedeem}} {{#if this.existingUserCanRedeem}}
<DButton @class="btn-primary" @action={{action "submit"}} @type="submit" @disabled={{this.submitDisabled}} @label="invites.accept_invite" /> <DButton @class="btn-primary" @action={{action "submit"}} @type="submit" @disabled={{this.submitDisabled}} @label="invites.accept_invite" />
{{else}} {{else}}
<div class="alert alert-error">{{i18n "invites.existing_user_cannot_redeem"}}</div> <div class="alert alert-error">{{this.existingUserCanRedeemError}}</div>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}

View File

@ -408,6 +408,7 @@ class InvitesController < ApplicationController
if current_user if current_user
info[:existing_user_id] = current_user.id info[:existing_user_id] = current_user.id
info[:existing_user_can_redeem] = invite.can_be_redeemed_by?(current_user) info[:existing_user_can_redeem] = invite.can_be_redeemed_by?(current_user)
info[:existing_user_can_redeem_error] = existing_user_can_redeem_error(invite)
info[:email] = current_user.email info[:email] = current_user.email
info[:username] = current_user.username info[:username] = current_user.username
end end
@ -493,4 +494,13 @@ class InvitesController < ApplicationController
end end
end end
end end
def existing_user_can_redeem_error(invite)
return if invite.can_be_redeemed_by?(current_user)
if invite.invited_users.exists?(user: current_user)
I18n.t("invite.existing_user_already_redemeed")
else
I18n.t("invite.existing_user_cannot_redeem")
end
end
end end

View File

@ -90,6 +90,10 @@ class Invite < ActiveRecord::Base
!redeemed? && !expired? && !deleted_at? && !destroyed? && link_valid? !redeemed? && !expired? && !deleted_at? && !destroyed? && link_valid?
end end
def redeemed_by_user?(redeeming_user)
self.invited_users.exists?(user: redeeming_user)
end
def redeemed? def redeemed?
if is_invite_link? if is_invite_link?
redemption_count >= max_redemptions_allowed redemption_count >= max_redemptions_allowed
@ -109,7 +113,7 @@ class Invite < ActiveRecord::Base
def can_be_redeemed_by?(user) def can_be_redeemed_by?(user)
return false if !self.redeemable? return false if !self.redeemable?
return true if self.email.blank? && self.domain.blank? return false if redeemed_by_user?(user)
return true if self.email.present? && email_matches?(user.email) return true if self.email.present? && email_matches?(user.email)
self.domain.present? && domain_matches?(user.email) self.domain.present? && domain_matches?(user.email)
end end

View File

@ -2097,7 +2097,6 @@ en:
name_label: "Name" name_label: "Name"
password_label: "Password" password_label: "Password"
existing_user_can_redeem: "Redeem your invitation to a topic or group." existing_user_can_redeem: "Redeem your invitation to a topic or group."
existing_user_cannot_redeem: "This invitation cannot be redeemed. Please ask the person who invited you to send you a new invitation."
password_reset: password_reset:
continue: "Continue to %{site_name}" continue: "Continue to %{site_name}"

View File

@ -250,6 +250,8 @@ en:
<p>Otherwise please <a href="%{base_url}/password-reset">Reset Password</a>.</p> <p>Otherwise please <a href="%{base_url}/password-reset">Reset Password</a>.</p>
not_found_template_link: | not_found_template_link: |
<p>This invitation to <a href="%{base_url}">%{site_name}</a> can no longer be redeemed. Please ask the person who invited you to send you a new invitation.</p> <p>This invitation to <a href="%{base_url}">%{site_name}</a> can no longer be redeemed. Please ask the person who invited you to send you a new invitation.</p>
existing_user_cannot_redeem: "This invitation cannot be redeemed. Please ask the person who invited you to send you a new invitation."
existing_user_already_redemeed: "You have already redeemed this invite link."
user_exists: "There's no need to invite <b>%{email}</b>, they already have an account!" user_exists: "There's no need to invite <b>%{email}</b>, they already have an account!"
invite_exists: "You already invited <b>%{email}</b>." invite_exists: "You already invited <b>%{email}</b>."
invalid_email: "%{email} isn't a valid email address." invalid_email: "%{email} isn't a valid email address."

View File

@ -126,6 +126,7 @@ RSpec.describe InvitesController do
json = JSON.parse(element.current_scope.attribute('data-preloaded').value) json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
invite_info = JSON.parse(json['invite_info']) invite_info = JSON.parse(json['invite_info'])
expect(invite_info['existing_user_can_redeem']).to eq(false) expect(invite_info['existing_user_can_redeem']).to eq(false)
expect(invite_info['existing_user_can_redeem_error']).to eq(I18n.t("invite.existing_user_cannot_redeem"))
end end
end end
@ -141,6 +142,22 @@ RSpec.describe InvitesController do
expect(invite_info['existing_user_can_redeem']).to eq(false) expect(invite_info['existing_user_can_redeem']).to eq(false)
end end
end end
it "does not allow the user to accept the invite when a multi-use invite link has already been redeemed by the user" do
invite.update!(email: nil, max_redemptions_allowed: 10)
expect(invite.redeem(redeeming_user: user)).not_to eq(nil)
get "/invites/#{invite.invite_key}"
expect(response.status).to eq(200)
expect(response.body).to have_tag('div#data-preloaded') do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
invite_info = JSON.parse(json['invite_info'])
expect(invite_info['existing_user_id']).to eq(user.id)
expect(invite_info['existing_user_can_redeem']).to eq(false)
expect(invite_info['existing_user_can_redeem_error']).to eq(I18n.t("invite.existing_user_already_redemeed"))
end
end
end end
it 'fails if invite does not exist' do it 'fails if invite does not exist' do