diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb
index 041ff8af2..e27869d82 100644
--- a/app/controllers/api/v1/accounts/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations_controller.rb
@@ -125,6 +125,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def destroy
+ authorize @conversation, :destroy?
::DeleteObjectJob.perform_later(@conversation, Current.user, request.ip)
head :ok
end
diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
index cce6609aa..a136f30bf 100644
--- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
@@ -168,6 +168,9 @@ export default {
// Don't show snooze if the conversation is already snoozed/resolved/pending
return this.status === wootConstants.STATUS_TYPE.OPEN;
},
+ isAdmin() {
+ return this.currentUser?.role === 'administrator';
+ },
},
mounted() {
this.$store.dispatch('inboxAssignableAgents/fetch', [this.inboxId]);
@@ -286,11 +289,13 @@ export default {
@click.stop="$emit('assignTeam', team)"
/>
-
-
+
+
+
+
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index 3f1a20037..922118b09 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -305,5 +305,6 @@ class Conversation < ApplicationRecord
end
end
+Conversation.include_mod_with('Audit::Conversation')
Conversation.include_mod_with('Concerns::Conversation')
Conversation.prepend_mod_with('Conversation')
diff --git a/app/policies/conversation_policy.rb b/app/policies/conversation_policy.rb
index 133cb3b02..931e17435 100644
--- a/app/policies/conversation_policy.rb
+++ b/app/policies/conversation_policy.rb
@@ -2,4 +2,8 @@ class ConversationPolicy < ApplicationPolicy
def index?
true
end
+
+ def destroy?
+ @account_user&.administrator?
+ end
end
diff --git a/enterprise/app/models/enterprise/audit/conversation.rb b/enterprise/app/models/enterprise/audit/conversation.rb
new file mode 100644
index 000000000..7ea08c343
--- /dev/null
+++ b/enterprise/app/models/enterprise/audit/conversation.rb
@@ -0,0 +1,7 @@
+module Enterprise::Audit::Conversation
+ extend ActiveSupport::Concern
+
+ included do
+ audited only: [], on: [:destroy]
+ end
+end
\ No newline at end of file
diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
index a3697be84..38bd649fe 100644
--- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
@@ -926,4 +926,63 @@ RSpec.describe 'Conversations API', type: :request do
end
end
end
+
+ describe 'DELETE /api/v1/accounts/{account.id}/conversations/:id' do
+ let(:conversation) { create(:conversation, account: account) }
+ let(:agent) { create(:user, account: account, role: :agent) }
+ let(:administrator) { create(:user, account: account, role: :administrator) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated agent' do
+ before do
+ create(:inbox_member, user: agent, inbox: conversation.inbox)
+ end
+
+ it 'returns unauthorized' do
+ delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unauthorized)
+ response_body = response.parsed_body
+ expect(response_body['error']).to eq('You are not authorized to do this action')
+ end
+ end
+
+ context 'when it is an authenticated administrator' do
+ before do
+ create(:inbox_member, user: administrator, inbox: conversation.inbox)
+ end
+
+ it 'successfully deletes the conversation' do
+ expect do
+ delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
+ headers: administrator.create_new_auth_token,
+ as: :json
+ end.to have_enqueued_job(DeleteObjectJob).with(conversation, administrator, anything)
+
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'can delete conversations from inboxes without direct access' do
+ other_inbox = create(:inbox, account: account)
+ other_conversation = create(:conversation, account: account, inbox: other_inbox)
+
+ expect do
+ delete "/api/v1/accounts/#{account.id}/conversations/#{other_conversation.display_id}",
+ headers: administrator.create_new_auth_token,
+ as: :json
+ end.to have_enqueued_job(DeleteObjectJob).with(other_conversation, administrator, anything)
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
end
diff --git a/spec/models/enterprise/audit/conversation_spec.rb b/spec/models/enterprise/audit/conversation_spec.rb
new file mode 100644
index 000000000..195e23c32
--- /dev/null
+++ b/spec/models/enterprise/audit/conversation_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+RSpec.describe 'Conversation Audit', type: :model do
+ let(:account) { create(:account) }
+ let(:conversation) { create(:conversation, account: account) }
+
+ before do
+ # Enable auditing for conversations
+ conversation.class.send(:include, Enterprise::Audit::Conversation) if defined?(Enterprise::Audit::Conversation)
+ end
+
+ describe 'audit logging on destroy' do
+ it 'creates an audit log when conversation is destroyed' do
+ skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
+
+ expect do
+ conversation.destroy!
+ end.to change { Audited::Audit.count }.by(1)
+
+ audit = Audited::Audit.last
+ expect(audit.auditable_type).to eq('Conversation')
+ expect(audit.action).to eq('destroy')
+ expect(audit.auditable_id).to eq(conversation.id)
+ end
+
+ it 'does not create audit log for other actions by default' do
+ skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
+
+ expect do
+ conversation.update!(priority: 'high')
+ end.not_to(change { Audited::Audit.count })
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/policies/conversation_policy_spec.rb b/spec/policies/conversation_policy_spec.rb
new file mode 100644
index 000000000..aaf23b20f
--- /dev/null
+++ b/spec/policies/conversation_policy_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+RSpec.describe ConversationPolicy, type: :policy do
+ subject { described_class }
+
+ let(:account) { create(:account) }
+ let(:conversation) { create(:conversation, account: account) }
+ let(:administrator) { create(:user, account: account, role: :administrator) }
+ let(:agent) { create(:user, account: account, role: :agent) }
+ let(:administrator_context) { { user: administrator, account: account, account_user: administrator.account_users.first } }
+ let(:agent_context) { { user: agent, account: account, account_user: agent.account_users.first } }
+
+ permissions :destroy? do
+ context 'when user is an administrator' do
+ it 'allows destroy' do
+ expect(subject).to permit(administrator_context, conversation)
+ end
+ end
+
+ context 'when user is an agent' do
+ it 'denies destroy' do
+ expect(subject).not_to permit(agent_context, conversation)
+ end
+ end
+ end
+
+ permissions :index? do
+ context 'for any authenticated user' do
+ it 'allows index' do
+ expect(subject).to permit(agent_context, conversation)
+ end
+ end
+ end
+end
\ No newline at end of file