diff --git a/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs
index 9332b72c82b..25cb311913a 100644
--- a/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs
+++ b/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs
@@ -184,24 +184,26 @@
{{/if}}
-
-
-
- |
+ {{#if invite.can_delete_invite}}
+
+
+
+ |
+ {{/if}}
{{/each}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js
index 0858762c635..e2ad32b9f56 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js
@@ -1,7 +1,6 @@
import { click, fillIn, visit } from "@ember/test-helpers";
import {
acceptance,
- count,
exists,
fakeTime,
loggedInUser,
@@ -57,15 +56,15 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
await visit("/u/eviltrout/invited/pending");
await click(".user-invite-buttons .btn:first-child");
- assert.ok(!exists("tbody tr"), "does not show invite before saving");
+ assert
+ .dom("table.user-invite-list tbody tr")
+ .exists({ count: 2 }, "is seeded with two rows");
await click(".btn-primary");
- assert.strictEqual(
- count("tbody tr"),
- 1,
- "adds invite to list after saving"
- );
+ assert
+ .dom("table.user-invite-list tbody tr")
+ .exists({ count: 3 }, "gets added to the list");
});
test("copying saves invite", async function (assert) {
diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
index a443bd9596d..3943e5b9fab 100644
--- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
+++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
@@ -224,7 +224,40 @@ export function applyDefaultHandlers(pretender) {
pretender.get("/u/eviltrout/invited.json", () => {
return response({
- invites: [],
+ invites: [
+ {
+ id: 8,
+ invite_key: "hMFT8G1oKP",
+ link: "http://localhost:3000/invites/hMFT8G1oKP",
+ email: "steak@cat.com",
+ domain: null,
+ emailed: false,
+ can_delete_invite: true,
+ custom_message: null,
+ created_at: "2023-06-01T04:47:13.195Z",
+ updated_at: "2023-06-01T04:47:13.195Z",
+ expires_at: "2023-08-30T04:47:00.000Z",
+ expired: false,
+ topics: [],
+ groups: [],
+ },
+ {
+ id: 9,
+ invite_key: "hMFT8G1WHA",
+ link: "http://localhost:3000/invites/hMFT8G1WHA",
+ email: "tomtom@cat.com",
+ domain: null,
+ emailed: false,
+ can_delete_invite: false,
+ custom_message: null,
+ created_at: "2023-06-01T04:47:13.195Z",
+ updated_at: "2023-06-01T04:47:13.195Z",
+ expires_at: "2023-08-30T04:47:00.000Z",
+ expired: false,
+ topics: [],
+ groups: [],
+ },
+ ],
can_see_invite_details: true,
counts: {
pending: 0,
diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-invited-show-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-invited-show-test.js
new file mode 100644
index 00000000000..686a340ad64
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/integration/components/user-invited-show-test.js
@@ -0,0 +1,19 @@
+import { visit } from "@ember/test-helpers";
+import { acceptance } from "discourse/tests/helpers/qunit-helpers";
+import { test } from "qunit";
+
+acceptance("User invites", function (needs) {
+ needs.user();
+
+ test("hides delete button based on can_delete_invite", async function (assert) {
+ await visit("/u/eviltrout/invited");
+
+ assert.dom("table.user-invite-list tbody tr").exists({ count: 2 });
+ assert
+ .dom("table.user-invite-list tbody tr:nth-child(1) button.cancel")
+ .exists();
+ assert
+ .dom("table.user-invite-list tbody tr:nth-child(2) button.cancel")
+ .doesNotExist();
+ });
+});
diff --git a/app/serializers/invite_serializer.rb b/app/serializers/invite_serializer.rb
index 5ca6021c9a1..9e49bdfa640 100644
--- a/app/serializers/invite_serializer.rb
+++ b/app/serializers/invite_serializer.rb
@@ -7,6 +7,7 @@ class InviteSerializer < ApplicationSerializer
:email,
:domain,
:emailed,
+ :can_delete_invite,
:max_redemptions_allowed,
:redemption_count,
:custom_message,
@@ -30,6 +31,10 @@ class InviteSerializer < ApplicationSerializer
object.emailed_status != Invite.emailed_status_types[:not_required]
end
+ def can_delete_invite
+ scope.is_admin? || object.invited_by_id == scope.current_user.id
+ end
+
def include_custom_message?
email.present?
end
diff --git a/spec/serializers/invite_serializer_spec.rb b/spec/serializers/invite_serializer_spec.rb
new file mode 100644
index 00000000000..c0a721b733f
--- /dev/null
+++ b/spec/serializers/invite_serializer_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.describe InviteSerializer do
+ describe "#can_delete_invite" do
+ fab!(:user) { Fabricate(:user) }
+ fab!(:admin) { Fabricate(:admin) }
+ fab!(:moderator) { Fabricate(:moderator) }
+ fab!(:invite_from_user) { Fabricate(:invite, invited_by: user) }
+ fab!(:invite_from_moderator) { Fabricate(:invite, invited_by: moderator) }
+
+ it "returns true for admin" do
+ serializer = InviteSerializer.new(invite_from_user, scope: Guardian.new(admin), root: false)
+
+ expect(serializer.as_json[:can_delete_invite]).to eq(true)
+ end
+
+ it "returns false for moderator" do
+ serializer =
+ InviteSerializer.new(invite_from_user, scope: Guardian.new(moderator), root: false)
+
+ expect(serializer.as_json[:can_delete_invite]).to eq(false)
+ end
+
+ it "returns true for inviter" do
+ serializer = InviteSerializer.new(invite_from_user, scope: Guardian.new(user), root: false)
+
+ expect(serializer.as_json[:can_delete_invite]).to eq(true)
+ end
+
+ it "returns false for plain user" do
+ serializer =
+ InviteSerializer.new(invite_from_moderator, scope: Guardian.new(user), root: false)
+
+ expect(serializer.as_json[:can_delete_invite]).to eq(false)
+ end
+ end
+end