FIX: Various invite system fixes (#13003)

* FIX: Ensure the same email cannot be invited twice

When creating a new invite with a duplicated email, the old invite will
be updated and returned. When updating an invite with a duplicated email
address, an error will be returned.

* FIX: not Ember helper does not exist

* FIX: Sync can_invite_to_forum? and can_invite_to?

The two methods should perform the same basic set of checks, such as
check must_approve_users site setting.

Ideally, one of the methods would call the other one or be merged and
that will happen in the future.

* FIX: Show invite to group if user is group owner
This commit is contained in:
Dan Ungureanu 2021-05-12 13:06:39 +03:00 committed by GitHub
parent b65af1193d
commit 60be1556fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 40 additions and 18 deletions

View File

@ -151,6 +151,11 @@ export default Controller.extend(
}); });
}, },
@discourseComputed("currentUser.staff", "currentUser.groups")
canInviteToGroup(staff, groups) {
return staff || groups.any((g) => g.owner);
},
@discourseComputed("type", "buffered.email") @discourseComputed("type", "buffered.email")
disabled(type, email) { disabled(type, email) {
if (type === "email") { if (type === "email") {

View File

@ -83,7 +83,7 @@
{{/if}} {{/if}}
{{#if showAdvanced}} {{#if showAdvanced}}
{{#if currentUser.staff}} {{#if canInviteToGroup}}
<div class="input-group invite-to-groups"> <div class="input-group invite-to-groups">
<label>{{i18n "user.invited.invite.add_to_groups"}}</label> <label>{{i18n "user.invited.invite.add_to_groups"}}</label>
{{group-chooser {{group-chooser

View File

@ -51,7 +51,7 @@
{{d-button {{d-button
icon="check" icon="check"
class="btn-primary" class="btn-primary"
disabled=(not users) disabled=(if users false true)
action=(action "notifyUsers") action=(action "notifyUsers")
}} }}
</div> </div>

View File

@ -71,10 +71,6 @@ class InvitesController < ApplicationController
end end
def create def create
if params[:email].present? && Invite.exists?(email: params[:email])
return render json: failed_json, status: 422
end
if params[:topic_id].present? if params[:topic_id].present?
topic = Topic.find_by(id: params[:topic_id]) topic = Topic.find_by(id: params[:topic_id])
raise Discourse::InvalidParameters.new(:topic_id) if topic.blank? raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
@ -153,6 +149,12 @@ class InvitesController < ApplicationController
old_email = invite.email.presence old_email = invite.email.presence
new_email = params[:email].presence new_email = params[:email].presence
if new_email
if Invite.where.not(id: invite.id).find_by(email: new_email.downcase, invited_by_id: current_user.id)&.redeemable?
return render_json_error(I18n.t("invite.invite_exists", email: new_email), status: 409)
end
end
if old_email != new_email if old_email != new_email
invite.emailed_status = if new_email && !params[:skip_email] invite.emailed_status = if new_email && !params[:skip_email]
Invite.emailed_status_types[:pending] Invite.emailed_status_types[:pending]

View File

@ -65,7 +65,12 @@ class CurrentUserSerializer < BasicUserSerializer
:can_review, :can_review,
def groups def groups
object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name } } owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set
object.visible_groups.pluck(:id, :name).map do |id, name|
group = { id: id, name: name }
group[:owner] = true if owned_group_ids.include?(id)
group
end
end end
def link_posting_access def link_posting_access

View File

@ -241,6 +241,7 @@ en:
not_found_template_link: | not_found_template_link: |
<p>The 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>The 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>
user_exists: "There's no need to invite <b>%{email}</b>, they <a href='%{base_path}/u/%{username}/summary'>already have an account!</a>" user_exists: "There's no need to invite <b>%{email}</b>, they <a href='%{base_path}/u/%{username}/summary'>already have an account!</a>"
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."
confirm_email: "<p>Youre almost done! We sent an activation mail to your email address. Please follow the instructions in the mail to activate your account.</p><p>If it doesnt arrive, check your spam folder.</p>" confirm_email: "<p>Youre almost done! We sent an activation mail to your email address. Please follow the instructions in the mail to activate your account.</p><p>If it doesnt arrive, check your spam folder.</p>"
cant_invite_to_group: "You are not allowed to invite users to specified group(s). Make sure you are owner of the group(s) you are trying to invite to." cant_invite_to_group: "You are not allowed to invite users to specified group(s). Make sure you are owner of the group(s) you are trying to invite to."

View File

@ -372,7 +372,8 @@ class Guardian
return false unless authenticated? return false unless authenticated?
is_topic = object.is_a?(Topic) is_topic = object.is_a?(Topic)
return true if is_admin? && !is_topic return true if is_admin? && !is_topic
return false if (SiteSetting.max_invites_per_day.to_i == 0 && !is_staff?) return false if SiteSetting.max_invites_per_day.to_i == 0 && !is_staff?
return false if SiteSetting.must_approve_users? && !is_staff?
return false unless can_see?(object) return false unless can_see?(object)
return false if groups.present? return false if groups.present?

View File

@ -579,6 +579,12 @@ describe Guardian do
expect(Guardian.new(trust_level_2).can_invite_to?(topic)).to be_truthy expect(Guardian.new(trust_level_2).can_invite_to?(topic)).to be_truthy
end end
it 'fails for normal users if must_approve_users' do
SiteSetting.must_approve_users = true
expect(Guardian.new(user).can_invite_to?(topic)).to be_falsey
expect(Guardian.new(admin).can_invite_to?(topic)).to be_truthy
end
describe 'for a private category for automatic and non-automatic group' do describe 'for a private category for automatic and non-automatic group' do
let(:category) do let(:category) do
Fabricate(:category, read_restricted: true).tap do |category| Fabricate(:category, read_restricted: true).tap do |category|

View File

@ -107,14 +107,6 @@ describe InvitesController do
post '/invites.json', params: { email: 'test@example.com' } post '/invites.json', params: { email: 'test@example.com' }
expect(response).to be_forbidden expect(response).to be_forbidden
end end
it 'fails for normal user if invite email already exists' do
Fabricate(:invite, invited_by: user, email: 'test@example.com')
post '/invites.json', params: { email: 'test@example.com' }
expect(response.status).to eq(422)
expect(response.parsed_body['failed']).to be_present
end
end end
context 'invite to topic' do context 'invite to topic' do
@ -191,15 +183,18 @@ describe InvitesController do
end end
context 'email invite' do context 'email invite' do
it 'does not allow users to send multiple invites to same email' do it 'creates invite once and updates it on successive calls' do
sign_in(user) sign_in(user)
post '/invites.json', params: { email: 'test@example.com' } post '/invites.json', params: { email: 'test@example.com' }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(Jobs::InviteEmail.jobs.size).to eq(1) expect(Jobs::InviteEmail.jobs.size).to eq(1)
invite = Invite.last
post '/invites.json', params: { email: 'test@example.com' } post '/invites.json', params: { email: 'test@example.com' }
expect(response.status).to eq(422) expect(response.status).to eq(200)
expect(response.parsed_body['id']).to eq(invite.id)
end end
it 'accept skip_email parameter' do it 'accept skip_email parameter' do
@ -328,6 +323,13 @@ describe InvitesController do
ensure ensure
RateLimiter.disable RateLimiter.disable
end end
it 'cannot create duplicated invites' do
Fabricate(:invite, invited_by: admin, email: 'test2@example.com')
put "/invites/#{invite.id}.json", params: { email: 'test2@example.com' }
expect(response.status).to eq(409)
end
end end
end end